AI code assistants have a long way to go
by prmph | 2025-05-29 15:25:20+00:00
Me>

There's something wrong with this snippet:

const tagFilters = searchTags.map(tag => tag.cat ? ["tags", "contains", JSON.stringify(tag)] as FilterSingle<ContentObjectXtended> : ["tags", "contains", tag.val] as FilterSingle<ContentObjectXtended> )

Claude>

The issue is in the filter value for categorized tags. You're JSON-stringifying the entire tag object, but the db stores tags as a JSONB array of objects. The correct approach is to use the JSONB containment operator with the proper object structure.

The fix:

const tagFilters = searchTags.map(tag => tag.cat ? ["tags", "contains", { val: tag.val, cat: tag.cat }] as FilterSingle<ContentObjectXtended> : ["tags", "contains", { val: tag.val }] as FilterSingle<ContentObjectXtended> )

This matches the JSONB array structure in the db where tags are stored as objects with val and optional cat properties. The PostgreSQL JSONB @> operator (which "contains" maps to) will properly match these object structures within the array.

Me>

Is the ternary operator even needed?

Claude>

No, the ternary operator is not needed. Looking at the db, all tags are stored as objects with a val property and an optional cat property.

const tagFilters = searchTags.map(tag => ["tags", "contains", { val: tag.val, cat: tag.cat }] as FilterSingle<ContentObjectXtended> )

This simplified version works because:

- When tag.cat is undefined, the object { val: tag.val, cat: undefined } will still match stored tags that have no category (where cat is null/undefined)

- PostgreSQL's JSONB containment operator handles the undefined/null cat property correctly

Me>

What about the map?

Claude>

You're right. Since each tag creates a separate filter condition and they need to be combined with AND logic, a simple map isn't sufficient. You need to create a compound filter.

const tagFilter = searchTags.length === 0 ? undefined : searchTags.length === 1 ? ["tags", "contains", { val: searchTags[0].val, cat: searchTags[0].cat }] as FilterSingle<ContentObjectXtended> : searchTags.reduce((acc, tag, index) => { const singleFilter: FilterSingle<ContentObjectXtended> = ["tags", "contains", { val: tag.val, cat: tag.cat }] return index === 1 ? [acc as FilterSingle<ContentObjectXtended>, "AND", singleFilter] as RecordFilter<ContentObjectXtended> : [acc, "AND", singleFilter] as RecordFilter<ContentObjectXtended> })

This creates:

- No filter if no search tags

- A single filter if one search tag

- A compound AND filter if multiple search tags

The reduce builds the nested structure: [filter1, "AND", [filter2, "AND", filter3]] for multiple tags, ensuring all specified tags must be present on matching objects.

Me>

No, you don't get it

Claude>

You're absolutely right. The tags column is a JSONB array, and a single "contains" operation can match multiple tags at once. You don't need to map over individual tags or create compound filters.

const tagFilter = searchTags.length > 0 ? ["tags", "contains", searchTags.map(tag => ({ val: tag.val, cat: tag.cat }))] as FilterSingle<ContentObjectXtended> : undefined

This passes the entire array of tag objects to the "contains" filter, which will use PostgreSQL's JSONB @> operator to check if the stored tags array contains all the specified tag objects.

Me>

Ok good, but why the map again?

Claude>

You're right again. Since searchTags is already an array of objects that match the db structure, no map is needed at all.

const tagFilter = searchTags.length > 0 ? ["tags", "contains", searchTags] as FilterSingle<ContentObjectXtended> : undefined

The searchTags array already has the correct structure to pass directly to the PostgreSQL JSONB containment operator.