[Apps] Fix - app-synced object should be searchable (#19206)

## Summary

- **Make app-synced objects searchable**: `isSearchable` was hardcoded
to `false` and the `searchVector` field was missing the `GENERATED
ALWAYS AS (...)` expression, causing all records to have a `NULL` search
vector and be excluded from search results. Fixed by defaulting
`isSearchable` to `true` (configurable via the object manifest),
computing the `asExpression` from the label identifier field, and
allowing the update-field-action-handler to handle the `null` → defined
`asExpression` transition.
- **Make `isSearchable` updatable on an object**: The property had
`toCompare: false` in the entity properties configuration, so updates
via the API were silently ignored and never persisted. Fixed by setting
`toCompare: true`.
This commit is contained in:
Marie
2026-04-02 19:14:37 +02:00
committed by GitHub
parent 43baf7b91c
commit 2d6c8be7df
34 changed files with 639 additions and 51 deletions
@@ -13,11 +13,6 @@ exports[`Sync application should fail due to object system fields integrity shou
"message": "System fields cannot be deleted",
"userFriendlyMessage": "System fields cannot be deleted",
},
{
"code": "FIELD_MUTATION_NOT_ALLOWED",
"message": "Cannot delete, please update the label identifier field first",
"userFriendlyMessage": "Cannot delete, please update the label identifier field first",
},
],
"flatEntityMinimalInformation": {
"name": "id",
@@ -79,6 +74,132 @@ exports[`Sync application should fail due to object system fields integrity shou
}
`;
exports[`Sync application should fail due to object system fields integrity when label identifier is non-searchable type (searchVector has no expression) 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"fieldMetadata": [
{
"errors": [
{
"code": "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": "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.",
},
],
"flatEntityMinimalInformation": {
"name": "searchVector",
"objectMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "fieldMetadata",
"status": "fail",
"type": "create",
},
],
"objectMetadata": [
{
"errors": [
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field searchVector is missing",
"userFriendlyMessage": "System field searchVector is missing",
"value": "searchVector",
},
{
"code": "INVALID_OBJECT_INPUT",
"message": "labelIdentifierFieldMetadataUniversalIdentifier validation failed: field type not compatible",
"userFriendlyMessage": "Field cannot be used as label identifier due to its type: should be of type UUID, text or full name",
},
],
"flatEntityMinimalInformation": {
"namePlural": "noSearchVectorExpressions",
"nameSingular": "noSearchVectorExpression",
"universalIdentifier": Any<String>,
},
"metadataName": "objectMetadata",
"type": "create",
},
],
},
"message": "Validation failed for 1 fieldMetadata, 1 objectMetadata",
"summary": {
"fieldMetadata": 1,
"objectMetadata": 1,
"totalErrors": 2,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
exports[`Sync application should fail due to object system fields integrity when object has TS_VECTOR field with wrong name (not searchVector) 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"fieldMetadata": [
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Field type TS_VECTOR must be named "searchVector", got "wrongSearchVector"",
"userFriendlyMessage": "Field type TS_VECTOR must be named "searchVector"",
"value": "wrongSearchVector",
},
{
"code": "INVALID_FIELD_INPUT",
"message": "Field type TS_VECTOR must be a system field",
"userFriendlyMessage": "Field type TS_VECTOR must be a system field",
"value": false,
},
],
"flatEntityMinimalInformation": {
"name": "wrongSearchVector",
"objectMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "fieldMetadata",
"status": "fail",
"type": "create",
},
],
"objectMetadata": [
{
"errors": [
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field searchVector is missing",
"userFriendlyMessage": "System field searchVector is missing",
"value": "searchVector",
},
],
"flatEntityMinimalInformation": {
"namePlural": "wrongTsVectorNames",
"nameSingular": "wrongTsVectorName",
"universalIdentifier": Any<String>,
},
"metadataName": "objectMetadata",
"type": "create",
},
],
},
"message": "Validation failed for 1 fieldMetadata, 1 objectMetadata",
"summary": {
"fieldMetadata": 1,
"objectMetadata": 1,
"totalErrors": 2,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
exports[`Sync application should fail due to object system fields integrity when object has id field with wrong type (TEXT instead of UUID) 1`] = `
{
"extensions": {
@@ -237,6 +358,43 @@ exports[`Sync application should fail due to object system fields integrity when
}
`;
exports[`Sync application should fail due to object system fields integrity when object has searchVector field with wrong type (TEXT instead of TS_VECTOR) 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"objectMetadata": [
{
"errors": [
{
"code": "INVALID_SYSTEM_FIELD",
"message": "System field searchVector has invalid type: expected TS_VECTOR, got TEXT",
"userFriendlyMessage": "System field searchVector has invalid type",
"value": "TEXT",
},
],
"flatEntityMinimalInformation": {
"namePlural": "wrongSearchVectorTypes",
"nameSingular": "wrongSearchVectorType",
"universalIdentifier": Any<String>,
},
"metadataName": "objectMetadata",
"type": "create",
},
],
},
"message": "Validation failed for 1 objectMetadata",
"summary": {
"objectMetadata": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
exports[`Sync application should fail due to object system fields integrity when object is created without any system fields (missing all 8 system fields) 1`] = `
{
"extensions": {
@@ -81,7 +81,7 @@ exports[`syncApplication should delete old field and create equivalent one when
"isCustom": true,
"isLabelSyncedWithName": false,
"isRemote": false,
"isSearchable": false,
"isSearchable": true,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataUniversalIdentifier": Any<String>,
@@ -311,7 +311,10 @@ exports[`syncApplication should delete old field and create equivalent one when
"standardOverrides": null,
"type": "TS_VECTOR",
"universalIdentifier": Any<String>,
"universalSettings": null,
"universalSettings": {
"asExpression": "to_tsvector('simple', COALESCE("id"::text, ''))",
"generatedType": "STORED",
},
"updatedAt": Any<String>,
},
{
@@ -434,7 +437,7 @@ exports[`syncApplication should return workspace migration actions on initial sy
"isCustom": true,
"isLabelSyncedWithName": false,
"isRemote": false,
"isSearchable": false,
"isSearchable": true,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataUniversalIdentifier": Any<String>,
@@ -664,7 +667,10 @@ exports[`syncApplication should return workspace migration actions on initial sy
"standardOverrides": null,
"type": "TS_VECTOR",
"universalIdentifier": Any<String>,
"universalSettings": null,
"universalSettings": {
"asExpression": "to_tsvector('simple', COALESCE("id"::text, ''))",
"generatedType": "STORED",
},
"updatedAt": Any<String>,
},
{
@@ -73,6 +73,44 @@ const buildObjectWithLabelField = ({
};
};
const buildDefaultObjectWithModifiedSearchVector = ({
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
searchVectorOverrides,
}: {
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
description: string;
searchVectorOverrides: Partial<ObjectManifest['fields'][number]>;
}): Pick<Manifest, 'objects' | 'fields'> => {
const defaultObject = buildDefaultObjectManifest({
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
});
return {
objects: [
{
...defaultObject,
fields: defaultObject.fields.map((field) =>
field.name === 'searchVector'
? ({ ...field, ...searchVectorOverrides } as (typeof defaultObject.fields)[number])
: field,
),
},
],
fields: [],
};
};
const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext =
[
{
@@ -153,6 +191,75 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext
),
},
},
{
title:
'when object has searchVector field with wrong type (TEXT instead of TS_VECTOR)',
context: {
manifest: buildManifest(
buildDefaultObjectWithModifiedSearchVector({
nameSingular: 'wrongSearchVectorType',
namePlural: 'wrongSearchVectorTypes',
labelSingular: 'Wrong SearchVector Type',
labelPlural: 'Wrong SearchVector Types',
description: 'Object with wrong searchVector field type',
searchVectorOverrides: {
type: FieldMetadataType.TEXT,
},
}),
),
},
},
{
title:
'when object has TS_VECTOR field with wrong name (not searchVector)',
context: {
manifest: buildManifest(
buildDefaultObjectWithModifiedSearchVector({
nameSingular: 'wrongTsVectorName',
namePlural: 'wrongTsVectorNames',
labelSingular: 'Wrong TsVector Name',
labelPlural: 'Wrong TsVector Names',
description: 'Object with TS_VECTOR field named incorrectly',
searchVectorOverrides: {
name: 'wrongSearchVector',
},
}),
),
},
},
{
title:
'when label identifier is non-searchable type (searchVector has no expression)',
context: (() => {
const nonSearchableFieldId = uuidv4();
return {
manifest: buildManifest({
objects: [
buildDefaultObjectManifest({
nameSingular: 'noSearchVectorExpression',
namePlural: 'noSearchVectorExpressions',
labelSingular: 'No SearchVector Expression',
labelPlural: 'No SearchVector Expressions',
description:
'Object whose label identifier is non-searchable, so searchVector has no expression',
labelIdentifierFieldMetadataUniversalIdentifier:
nonSearchableFieldId,
additionalFields: [
{
universalIdentifier: nonSearchableFieldId,
type: FieldMetadataType.NUMBER,
name: 'quantity',
label: 'Quantity',
},
],
}),
],
fields: [],
}),
};
})(),
},
];
describe('Sync application should fail due to object system fields integrity', () => {
@@ -207,12 +314,25 @@ describe('Sync application should fail due to object system fields integrity', (
);
it('should fail when trying to delete a system field after a successful sync', async () => {
const labelIdentifierFieldUniversalIdentifier = uuidv4();
const testObject = buildDefaultObjectManifest({
nameSingular: 'deleteSystemFieldObject',
namePlural: 'deleteSystemFieldObjects',
labelSingular: 'Delete System Field Object',
labelPlural: 'Delete System Field Objects',
description: 'Object for testing system field deletion',
labelIdentifierFieldMetadataUniversalIdentifier:
labelIdentifierFieldUniversalIdentifier,
additionalFields: [
{
universalIdentifier: labelIdentifierFieldUniversalIdentifier,
type: FieldMetadataType.TEXT,
name: 'labelIdentifierField',
label: 'Label Identifier Field',
description: 'Label identifier field',
icon: 'IconTextCaption',
},
],
});
const validManifest = buildManifest({
@@ -244,12 +364,25 @@ describe('Sync application should fail due to object system fields integrity', (
}, 60000);
it('should fail when trying to update a system field after a successful sync', async () => {
const labelIdentifierFieldUniversalIdentifier = uuidv4();
const testObject = buildDefaultObjectManifest({
nameSingular: 'updateSystemFieldObject',
namePlural: 'updateSystemFieldObjects',
labelSingular: 'Update System Field Object',
labelPlural: 'Update System Field Objects',
description: 'Object for testing system field update',
labelIdentifierFieldMetadataUniversalIdentifier:
labelIdentifierFieldUniversalIdentifier,
additionalFields: [
{
universalIdentifier: labelIdentifierFieldUniversalIdentifier,
type: FieldMetadataType.TEXT,
name: 'labelIdentifierField',
label: 'Label Identifier Field',
description: 'Label identifier field',
icon: 'IconTextCaption',
},
],
});
const validManifest = buildManifest({
@@ -11,6 +11,7 @@ export const buildDefaultObjectManifest = ({
icon = 'IconTicket',
additionalFields = [],
universalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
}: {
nameSingular: string;
namePlural: string;
@@ -20,12 +21,15 @@ export const buildDefaultObjectManifest = ({
icon?: string;
additionalFields?: ObjectManifest['fields'];
universalIdentifier?: string;
labelIdentifierFieldMetadataUniversalIdentifier?: string;
}): ObjectManifest => {
const idFieldUniversalIdentifier = uuidv4();
return {
universalIdentifier: universalIdentifier ?? uuidv4(),
labelIdentifierFieldMetadataUniversalIdentifier: idFieldUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier:
labelIdentifierFieldMetadataUniversalIdentifier ??
idFieldUniversalIdentifier,
nameSingular,
namePlural,
labelSingular,
@@ -12,7 +12,7 @@ exports[`Object metadata update should fail when labelIdentifier is not a TEXT o
{
"code": "INVALID_OBJECT_INPUT",
"message": "labelIdentifierFieldMetadataUniversalIdentifier validation failed: field type not compatible",
"userFriendlyMessage": "Field cannot be used as label identifier",
"userFriendlyMessage": "Field cannot be used as label identifier due to its type: should be of type UUID, text or full name",
},
],
"flatEntityMinimalInformation": {