[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:
+163
-5
@@ -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": {
|
||||
|
||||
+10
-4
@@ -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>,
|
||||
},
|
||||
{
|
||||
|
||||
+133
@@ -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({
|
||||
|
||||
+5
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user