Skip to content

Hotfix v0.3.3: search covers hidden columns and translation fields (#24)#56

Merged
smartlabsAT merged 1 commit into
mainfrom
hotfix/v0.3.3-issue-24-search
May 10, 2026
Merged

Hotfix v0.3.3: search covers hidden columns and translation fields (#24)#56
smartlabsAT merged 1 commit into
mainfrom
hotfix/v0.3.3-issue-24-search

Conversation

@smartlabsAT
Copy link
Copy Markdown
Owner

Summary

Closes #24 — search now correctly covers hidden columns and translation fields, plus a related side-bug fix for the translation collection lookup.

Reported in #24 over the course of 8 months by @DjLawRee, @OSZII, and several other users. Two distinct sub-bugs are addressed in this PR.


Sub-Bug A — hidden columns ignored (@DjLawRee)

The all-fields fallback inside buildSearchFilter was gated by conditions.length === 0, meaning it only ran when no visible column produced a clause. As soon as any visible string column contributed, every other searchable column the user had not added to the layout was silently skipped — even legitimate ones with meta.hidden !== true.

Fix: the fallback is now cumulative. Visible-pass and hidden-pass run sequentially, deduplicating via processedFields so no field contributes twice.

Sub-Bug B — top-level translations alias not searched (@OSZII)

When the translations alias is configured at the top level (just translations as a column), the visible pass landed in the type check, found alias ≠ string/text, and produced no clause at all. OSZII reported the inability to find ""Berg"" in a translation's name column.

Fix: the visible pass now detects translation aliases via field.meta.special.includes('translations') (works for renamed aliases like i18n / localizations too — not just the literal field name), resolves the translations collection through the relations store, enumerates its searchable string/text columns (excluding M2O FKs and hidden fields), and emits one outer-_or clause per column.

Critical detail discovered during integration testing

Clauses are emitted as one outer-_or entry per column:

{ _or: [
  { translations: { _some: { title: { _icontains: 'Berg' } } } },
  { translations: { _some: { excerpt: { _icontains: 'Berg' } } } },
  { translations: { _some: { body: { _icontains: 'Berg' } } } },
]}

NOT wrapped in a single _some._or:

{ translations: { _some: { _or: [...] } } }   ← matches every row in Directus 11

Directus' query parser does not evaluate _or correctly inside _some — it returns every row. My initial implementation used the wrapped form, all 24 unit tests passed, but the live API call returned 30/30 items instead of 1. The browser-level integration test was what caught it. Tests now pin the correct unwrapped form.

Side-fix — wrong relations property in existing code

While building the new helper I noticed useTableFields.ts:73 and the inline implementation in super-table.vue:534 were using relation.related_collection || relation.collection to find the translations collection. For parent → translations O2M relations this returned the parent collection itself, not the translations collection.

The bug was masked by a hardcoded commonTranslationFields fallback (name, title, description, content, subtitle). Custom translation field names like body, slug, or summary did not resolve correctly.

Fix: both call sites now use the new shared helper getTranslationFieldMetadata in src/utils/resolveTranslationsCollection.ts, which:

  • reads relation.meta.many_collection as the canonical path (with relation.collection as defensive fallback), and
  • delegates entirely to the schema. The hardcoded field list is removed.

Code organization

  • src/utils/buildSearchFilter.ts — pure function extracted from super-table.vue (was 120 LOC inline) for direct unit testability without Vue mounting.
  • src/utils/resolveTranslationsCollection.ts — shared helper used by both super-table.vue and useTableFields.ts. No more duplicate inline implementations.
  • 6 files changed: 1235 insertions, 192 deletions.

Test plan

  • 37 new unit tests in tests/unit/utils/buildSearchFilter.test.ts and resolveTranslationsCollection.test.ts covering empty/whitespace input, visible strings/UUIDs/integers, dot-notation translations, top-level translations (single/multi-column, with/without renamed alias, M2O exclusion, FK exclusion, hidden-field exclusion, missing-relation graceful skip, multiple translations relations), and the cumulative hidden-field fallback.
  • TypeScript strict mode clean (pnpm run type-check)
  • ESLint --max-warnings=0 clean
  • Prettier formatting clean
  • All 133 existing tests still pass
  • Build clean

End-to-end browser verification (18 scenarios)

Tested live against three reproduction collections plus a realistic multi-relation articles collection:

# Scenario Result
1 Cumulative hidden pass — fact-check in internal_notes (not as column) ✓ found
2 meta.hidden=true exclusion — SECRET-ADMIN (secret_admin_notes) ✓ 0 results, no clause emitted
3 DE translation + umlaut — Hochzeit ✓ found
4 EN translation — wedding ✓ found
5 Multi-relation + RENAMED alias — Weltklasse via i18n_seo.meta_description ✓ 8 clauses emitted, 1 result
6 Integer _eq1500 on view_count ✓ found
7 Pagination + search — bulk ✓ 25 page-1 rows
8 Sort + search combined — Italien + sort -slug ✓ both applied
9 Case-insensitivity — HOCHZEIT matches Hochzeit ✓ found
10 LIKE wildcard % ⚠ Directus' default _icontains behavior, consistent with native search
11 SQL injection — O'Brien'); DROP TABLE articles;-- ✓ table intact, parameterized
12 Search clear → all items return ✓ request without filter
13 CJK multibyte — 日本語 matches translation body ✓ found
14 Whitespace-only query ✓ early return, no filter
15 UUID search on hidden PK ✓ consistent with native (meta.hidden=true excludes)
16 Single character a (deduplication) ✓ 25 unique rows, no duplicates
17 Layout-switch with active search ⚠ Directus-side, out of scope
18 Existing test_bug_44 regression smoke test ✓ unchanged

What is intentionally out of scope

  • JSON field deep-search — Directus' _icontains does not support json columns (raises 400); native search has the same limitation. Sub-Bug A covers DjLawRee's actual use case (term in a hidden text/string column rather than truly inside a JSON tree).
  • % and _ LIKE-wildcard escaping — Directus' _icontains operator does not escape these by default; native search behaves identically. Documenting, not fixing here.
  • Layout-switch search persistence — Directus-internal behavior, not in our filter logic.

Reviewer notes

  • The two new files are pure-function helpers with no Vue reactivity; they accept stores via duck-typed interfaces (FieldsStoreLike, RelationsStoreLike) so tests work without Pinia.
  • The _some._or Directus parser quirk is documented inline in buildSearchFilter.ts to prevent future regressions.
  • Refs the previous-related fix Bug: Extension requests non-existent .title field in translations causing errors #34 (the original .title translation hardcoded-fallback bug for translations); this PR removes the residual hardcoded fallback at its root.

Two sub-bugs reported by @DjLawRee and @OSZII over the course of
8 months, plus a related side-bug discovered during code review.

Sub-Bug A (hidden columns): the all-fields fallback in
buildSearchFilter ran only when no visible column produced a clause,
so any layout with a visible string column silently skipped every
other searchable column. The fallback is now cumulative — visible
and hidden passes run sequentially with processedFields
deduplication.

Sub-Bug B (translations alias): top-level translations aliases were
not searched at all. Detection now via field.meta.special (works for
renamed aliases like i18n / localizations), relations store
resolves the translations collection, one outer-_or clause per
searchable column. Clauses are NOT wrapped in
`{ _some: { _or: [...] } }` because Directus' query parser does
not evaluate _or inside _some — it matches every row. Bug caught
by integration testing during the fix.

Side-fix: useTableFields.ts and super-table.vue used
`relation.related_collection || relation.collection` for the
translations collection lookup. For parent → translations
relations this returned the parent itself; the bug was masked
by a hardcoded commonTranslationFields fallback (name, title,
description, content, subtitle). Both call sites now use a new
shared helper that reads `relation.meta.many_collection` and
trusts the schema entirely — custom field names (body, slug,
summary, …) resolve correctly.

Code:
  - new pure function src/utils/buildSearchFilter.ts (extracted
    from super-table.vue for direct unit testability)
  - new src/utils/resolveTranslationsCollection.ts (shared by
    super-table.vue and useTableFields.ts)

37 new unit tests, 18 E2E browser scenarios verified including
SQL-injection safety, CJK multibyte, case-insensitivity,
sort+search, pagination, multi-relation, M2O FK exclusion,
meta.hidden exclusion, and deduplication.

Closes #24
@smartlabsAT smartlabsAT linked an issue May 9, 2026 that may be closed by this pull request
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

Quality Check Results

TypeScript Type Check

Passed - No type errors found

ESLint

Passed - No linting errors

Prettier Format Check

Passed - Code is properly formatted

Build

Passed - Extension builds successfully


Updated: 2026-05-09T22:14:55.067Z

@smartlabsAT smartlabsAT merged commit e495a40 into main May 10, 2026
18 checks passed
smartlabsAT added a commit that referenced this pull request May 10, 2026
Combined two non-overlapping import groups in src/super-table.vue:
- usePermissions, useTranslationLanguages (PR #57 — permission filtering)
- getTranslationFieldMetadata, buildSearchFilter (PR #56 — search hotfix)

All 171 tests pass post-merge (134 from PR #57 + 37 from PR #56).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Extension requests non-existent .title field in translations causing errors Directus native search does not work with super-table

1 participant