From 57aa395ff6261d2a0b85d2bc7a6e85e88fa06e09 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 10 Mar 2026 19:09:25 +0100 Subject: [PATCH] Increase search field limit. (#1296) * Increase search field limit. * Fix tests --- backend/i18n/frontend_de.json | 2 +- backend/i18n/frontend_en.json | 2 +- backend/i18n/frontend_fr.json | 2 +- backend/i18n/frontend_it.json | 2 +- backend/i18n/frontend_nl.json | 2 +- backend/i18n/frontend_pt.json | 2 +- backend/i18n/frontend_zh.json | 2 +- backend/i18n/source/frontend_en.json | 2 +- .../DomainObject/Guards/GuardSchema.cs | 4 +- .../DomainObject/Guards/GuardSchemaTests.cs | 4 +- .../src/app/framework/utils/types.spec.ts | 55 +++++++++++++++++++ frontend/src/app/framework/utils/types.ts | 21 ++++++- .../src/app/shared/state/schemas.forms.ts | 2 +- 13 files changed, 87 insertions(+), 15 deletions(-) diff --git a/backend/i18n/frontend_de.json b/backend/i18n/frontend_de.json index 12da9a67b..2584af7f6 100644 --- a/backend/i18n/frontend_de.json +++ b/backend/i18n/frontend_de.json @@ -1049,7 +1049,7 @@ "schemas.schemaNameValidationMessage": "Der Name darf nur Buchstaben, Zahlen und Bindestriche enthalten.", "schemas.schemaTagsHint": "Tags zur Annotation Ihres Schemas für Automatisierungsprozesse.", "schemas.searchFields": "Search Fields", - "schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.", + "schemas.searchFieldsHelp": "The default search uses full-text search (indexed) across all available data. This is fast and powerful, but only matches whole words or stems, so partial terms may return no results.\n\nYou can define up to 4 additional field search fields, which use a simple contains match. This means a result is returned whenever the entered text appears anywhere within that field, making it ideal for catching partial matches the full-text search might miss.\n\nIf you need more than 4 extra fields, use a schema script to combine multiple fields into one.", "schemas.searchFieldsHint": "The search fields", "schemas.searchPlaceholder": "Suchen", "schemas.showFieldFailed": "Fehler beim Anzeigen des Feldes. Bitte neu laden.", diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 321ec093d..b7872e5a4 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -1049,7 +1049,7 @@ "schemas.schemaNameValidationMessage": "Name can only contain letters, numbers and dashes.", "schemas.schemaTagsHint": "Tags to annotate your schema for automation processes.", "schemas.searchFields": "Search Fields", - "schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.", + "schemas.searchFieldsHelp": "The default search uses full-text search (indexed) across all available data. This is fast and powerful, but only matches whole words or stems, so partial terms may return no results.\n\nYou can define up to 4 additional field search fields, which use a simple contains match. This means a result is returned whenever the entered text appears anywhere within that field, making it ideal for catching partial matches the full-text search might miss.\n\nIf you need more than 4 extra fields, use a schema script to combine multiple fields into one.", "schemas.searchFieldsHint": "The search fields", "schemas.searchPlaceholder": "Search", "schemas.showFieldFailed": "Failed to show field. Please reload.", diff --git a/backend/i18n/frontend_fr.json b/backend/i18n/frontend_fr.json index b3150db43..13d2d2523 100644 --- a/backend/i18n/frontend_fr.json +++ b/backend/i18n/frontend_fr.json @@ -1049,7 +1049,7 @@ "schemas.schemaNameValidationMessage": "Le nom ne peut contenir que des lettres, des chiffres et des tirets.", "schemas.schemaTagsHint": "Balises pour annoter votre schéma pour les processus d'automatisation.", "schemas.searchFields": "Search Fields", - "schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.", + "schemas.searchFieldsHelp": "The default search uses full-text search (indexed) across all available data. This is fast and powerful, but only matches whole words or stems, so partial terms may return no results.\n\nYou can define up to 4 additional field search fields, which use a simple contains match. This means a result is returned whenever the entered text appears anywhere within that field, making it ideal for catching partial matches the full-text search might miss.\n\nIf you need more than 4 extra fields, use a schema script to combine multiple fields into one.", "schemas.searchFieldsHint": "The search fields", "schemas.searchPlaceholder": "Recherche", "schemas.showFieldFailed": "Impossible d'afficher le champ. Veuillez recharger.", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index c6e002ea2..a351329d8 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -1049,7 +1049,7 @@ "schemas.schemaNameValidationMessage": "Il nome può contenere solo lettere, numeri e trattini.", "schemas.schemaTagsHint": "Tag per descrivere il tuo schema per i processi automatici.", "schemas.searchFields": "Search Fields", - "schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.", + "schemas.searchFieldsHelp": "The default search uses full-text search (indexed) across all available data. This is fast and powerful, but only matches whole words or stems, so partial terms may return no results.\n\nYou can define up to 4 additional field search fields, which use a simple contains match. This means a result is returned whenever the entered text appears anywhere within that field, making it ideal for catching partial matches the full-text search might miss.\n\nIf you need more than 4 extra fields, use a schema script to combine multiple fields into one.", "schemas.searchFieldsHint": "The search fields", "schemas.searchPlaceholder": "Cerca negli schemi...", "schemas.showFieldFailed": "Non è stato possibile mostrare il campo. Per favore ricarica.", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 0e694ec61..d3a8ecc06 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -1049,7 +1049,7 @@ "schemas.schemaNameValidationMessage": "Naam mag alleen letters, cijfers en streepjes bevatten.", "schemas.schemaTagsHint": "Tags om uw schema voor automatiseringsprocessen te annoteren.", "schemas.searchFields": "Search Fields", - "schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.", + "schemas.searchFieldsHelp": "The default search uses full-text search (indexed) across all available data. This is fast and powerful, but only matches whole words or stems, so partial terms may return no results.\n\nYou can define up to 4 additional field search fields, which use a simple contains match. This means a result is returned whenever the entered text appears anywhere within that field, making it ideal for catching partial matches the full-text search might miss.\n\nIf you need more than 4 extra fields, use a schema script to combine multiple fields into one.", "schemas.searchFieldsHint": "The search fields", "schemas.searchPlaceholder": "Zoek schema's ...", "schemas.showFieldFailed": "Kan veld niet weergeven. Laad opnieuw.", diff --git a/backend/i18n/frontend_pt.json b/backend/i18n/frontend_pt.json index c79b8e954..7d39385a4 100644 --- a/backend/i18n/frontend_pt.json +++ b/backend/i18n/frontend_pt.json @@ -1049,7 +1049,7 @@ "schemas.schemaNameValidationMessage": "O nome só pode conter letras, números e traços.", "schemas.schemaTagsHint": "Etiquetas para anotar o seu esquema para processos de automação.", "schemas.searchFields": "Search Fields", - "schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.", + "schemas.searchFieldsHelp": "The default search uses full-text search (indexed) across all available data. This is fast and powerful, but only matches whole words or stems, so partial terms may return no results.\n\nYou can define up to 4 additional field search fields, which use a simple contains match. This means a result is returned whenever the entered text appears anywhere within that field, making it ideal for catching partial matches the full-text search might miss.\n\nIf you need more than 4 extra fields, use a schema script to combine multiple fields into one.", "schemas.searchFieldsHint": "The search fields", "schemas.searchPlaceholder": "Pesquisar", "schemas.showFieldFailed": "Falhou em mostrar o campo. Por favor, recarregue.", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index ea6e99713..d9d8ebdd6 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -1049,7 +1049,7 @@ "schemas.schemaNameValidationMessage": "名称只能包含字母、数字和破折号。", "schemas.schemaTagsHint": "用于注释自动化流程Schemas的标签。", "schemas.searchFields": "Search Fields", - "schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.", + "schemas.searchFieldsHelp": "The default search uses full-text search (indexed) across all available data. This is fast and powerful, but only matches whole words or stems, so partial terms may return no results.\n\nYou can define up to 4 additional field search fields, which use a simple contains match. This means a result is returned whenever the entered text appears anywhere within that field, making it ideal for catching partial matches the full-text search might miss.\n\nIf you need more than 4 extra fields, use a schema script to combine multiple fields into one.", "schemas.searchFieldsHint": "The search fields", "schemas.searchPlaceholder": "搜索Schemas...", "schemas.showFieldFailed": "显示字段失败。请重新加载。", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 321ec093d..b7872e5a4 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -1049,7 +1049,7 @@ "schemas.schemaNameValidationMessage": "Name can only contain letters, numbers and dashes.", "schemas.schemaTagsHint": "Tags to annotate your schema for automation processes.", "schemas.searchFields": "Search Fields", - "schemas.searchFieldsHelp": "The normal search uses **full-text search** across the available data.\n\nYou can optionally define **1–3 additional search fields**. These fields are included in the search using a simple **“contains” match**, meaning results are returned when the entered text appears anywhere within those fields.", + "schemas.searchFieldsHelp": "The default search uses full-text search (indexed) across all available data. This is fast and powerful, but only matches whole words or stems, so partial terms may return no results.\n\nYou can define up to 4 additional field search fields, which use a simple contains match. This means a result is returned whenever the entered text appears anywhere within that field, making it ideal for catching partial matches the full-text search might miss.\n\nIf you need more than 4 extra fields, use a schema script to combine multiple fields into one.", "schemas.searchFieldsHint": "The search fields", "schemas.searchPlaceholder": "Search", "schemas.showFieldFailed": "Failed to show field. Please reload.", diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs index 8759fda53..e40e24e06 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardSchema.cs @@ -116,9 +116,9 @@ public static class GuardSchema Validate.It(e => { - if (command.Properties.SearchFields != null && command.Properties.SearchFields.Count > 3) + if (command.Properties.SearchFields != null && command.Properties.SearchFields.Count > 4) { - e(Not.Between("Size", 1, 3), "Properties.SearchFields"); + e(Not.Between("Size", 1, 4), "Properties.SearchFields"); } }); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs index ac5d20183..06249f80f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/GuardSchemaTests.cs @@ -664,12 +664,12 @@ public class GuardSchemaTests : GivenContext, IClassFixture { Properties = new SchemaProperties() { - SearchFields = FieldNames.Create("a", "b", "c", "d"), + SearchFields = FieldNames.Create("a", "b", "c", "d", "e"), }, }; ValidationAssert.Throws(() => GuardSchema.CanUpdateSchema(command), - new ValidationError("Size must be between 1 and 3.", "Properties.SearchFields")); + new ValidationError("Size must be between 1 and 4.", "Properties.SearchFields")); } private CreateSchema CreateCommand(CreateSchema command) diff --git a/frontend/src/app/framework/utils/types.spec.ts b/frontend/src/app/framework/utils/types.spec.ts index f86ace01a..2f56fb80a 100644 --- a/frontend/src/app/framework/utils/types.spec.ts +++ b/frontend/src/app/framework/utils/types.spec.ts @@ -290,6 +290,61 @@ describe('Types', () => { expect(result).toEqual('Hello, 1, Yes'); }); + + describe('Hashing', () => { + it('should hash a string', () => { + const result = Types.fnv1a('hello'); + expect(result).toBe(Types.fnv1a('hello')); // deterministic + expect(typeof result).toBe('string'); + }); + + it('should hash a number', () => { + expect(Types.fnv1a(42)).toBe(Types.fnv1a(42)); + expect(Types.fnv1a(42)).not.toBe(Types.fnv1a(43)); + }); + + it('should hash a boolean', () => { + expect(Types.fnv1a(true)).toBe(Types.fnv1a(true)); + expect(Types.fnv1a(true)).not.toBe(Types.fnv1a(false)); + }); + + it('should hash an object', () => { + expect(Types.fnv1a({ a: 1 })).toBe(Types.fnv1a({ a: 1 })); + expect(Types.fnv1a({ a: 1 })).not.toBe(Types.fnv1a({ a: 2 })); + }); + + it('should hash an array', () => { + expect(Types.fnv1a([1, 2, 3])).toBe(Types.fnv1a([1, 2, 3])); + expect(Types.fnv1a([1, 2, 3])).not.toBe(Types.fnv1a([1, 2, 4])); + }); + + it('should hash null and undefined', () => { + expect(typeof Types.fnv1a(null)).toBe('string'); + expect(typeof Types.fnv1a(undefined)).toBe('string'); + expect(Types.fnv1a(null)).not.toBe(Types.fnv1a(undefined)); + }); + + it('should return a base64 encoded string with identifier', () => { + const result = Types.hashWithId('hello', 'session'); + + expect(() => atob(result)).not.toThrow(); + expect(atob(result)).toMatch(/^session:/); + }); + + it('should embed the correct hash in the encoded result', () => { + const hash = Types.fnv1a('hello'); + + expect(atob(Types.hashWithId('hello', 'session'))).toBe(`session:${hash}`); + }); + + it('should produce different results for different identifiers', () => { + expect(Types.hashWithId('hello', 'a')).not.toBe(Types.hashWithId('hello', 'b')); + }); + + it('should produce different results for different values', () => { + expect(Types.hashWithId('hello', 'id')).not.toBe(Types.hashWithId('world', 'id')); + }); + }) }); class MyClass { diff --git a/frontend/src/app/framework/utils/types.ts b/frontend/src/app/framework/utils/types.ts index 3cf0b3fd0..389b70fca 100644 --- a/frontend/src/app/framework/utils/types.ts +++ b/frontend/src/app/framework/utils/types.ts @@ -97,8 +97,7 @@ export module Types { return true; } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare - return Types.isUndefined(value) === true || Types.isNull(value) === true; + return Types.isUndefined(value) || Types.isNull(value); } export function clone(lhs: T): T { @@ -231,4 +230,22 @@ export module Types { return result; } + + export function fnv1a(value: unknown): string { + const text = JSON.stringify(value) ?? String(value); + + let hash = 2166136261; + for (let i = 0; i < text.length; i++) { + hash ^= text.charCodeAt(i); + hash = (hash * 16777619) >>> 0; + } + + return hash.toString(16); + } + + export function hashWithId(value: unknown, identifier: string): string { + const hash = fnv1a(value); + const combined = `${identifier}:${hash}`; + return btoa(combined); + } } diff --git a/frontend/src/app/shared/state/schemas.forms.ts b/frontend/src/app/shared/state/schemas.forms.ts index 19feffae7..5b6ec0d04 100644 --- a/frontend/src/app/shared/state/schemas.forms.ts +++ b/frontend/src/app/shared/state/schemas.forms.ts @@ -422,7 +422,7 @@ export class EditSchemaForm extends Form