AI code assistants have a long way to go
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.
No comments yet