EntityPropertyQuery with property from related entity

Hi, I am working on creating a EntityPropertyQuery for my App entity. I want the user to be able to use Shortcuts to search by a property in a related entity, but I'm struggling with how the syntax for that looks.

I know the documentation for 'EntityPropertyQuery' suggests that this should be possible with a different initializer for the 'QueryProperty' that takes in a 'entityProvider' but I can't figure out how it works.

For e.g. my CJPersonAppEntity has 'emails', which is of type CJEmailAppEntity, which has a property 'emailAddress'. I want the user to be able to find the 'person' by looking up an email address.

When I try to provide this as a Property to filter by, inside CJPersonAppEntityQuery, but I get a syntax error:

static var properties = QueryProperties {
Property(\CJPersonEmailAppEntity.$emailAddress, entityProvider: { person in
                person.emails  // error
            }) {
                EqualToComparator { NSPredicate(format: "emailAddress == %@", $0) }
                ContainsComparator { NSPredicate(format: "emailAddress CONTAINS %@", $0) }
            }
}

The error says "Cannot convert value of type '[CJPersonEmailAppEntity]' to closure result type 'CJPersonEmailAppEntity'"

So it's not expecting an array, but an individual email item. But how do I provide that without running the predicate query that's specified in the closure?

So I tried something like this , just returning something without worrying about correctness:

            Property(\CJPersonEmailAppEntity.$emailAddress, entityProvider: { person in
                person.emails.first ?? CJPersonEmailAppEntity() // satisfy compiler
            }) {
                EqualToComparator { NSPredicate(format: "emailAddress == %@", $0) }
                ContainsComparator { NSPredicate(format: "emailAddress CONTAINS %@", $0) }
            }

and it built the app, but failed on another the step 'Extracting app intents metadata':

error: Entity CJPersonAppEntity does not contain a property named emailAddress. Ensure that the property is wrapped with an @Property property wrapper

So I'm not sure what the correct syntax for handling this case is, and I can't find any other examples of how it's done. Would love some feedback for this.

Do you get the same results with just the relevant code in a small test project? I'd like to see a buildable test project with the entity types defined to understand this better.

If you're not familiar with preparing a test project, take a look at Creating a test project.

— Ed Ford,  DTS Engineer

Hi, Yes I get the same results on a new project. I have created an example project and attached it here. It's based on a basic Core Data app, with an "Item" entity, that has a one-to-many relationship with a "Tag" entity. When I convert these into "App Entities", I can create a query for the properties of the "Item" entity, and another query for the "Tags" entity, but I can't figure out how to create a query in "Item" that would also look for associated 'tags'.

I can't seem to attach a zip file containing the app project to this post for some reason. But the code is fairly simple.

This is the Item AppEntity:

import Foundation
import AppIntents

struct ItemsAppEntity: AppEntity {
    static var defaultQuery = ItemsAppEntityQuery()
    
    var id: String
    
    @Property(title: "Timestamp")
    var timestamp: Date
    
    @Property(title: "Tags")
    var tags: [CJTagItemsAppEntity]
    
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Test Item")
    
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(timestamp.formatted())")
    }
    
    struct ItemsAppEntityQuery: EntityPropertyQuery {
        
        static var sortingOptions = SortingOptions {
            SortableBy(\ItemsAppEntity.$timestamp)
        }
        
        typealias ComparatorMappingType = NSPredicate
     
        static var properties = QueryProperties {
            Property(\ItemsAppEntity.$timestamp) {
                LessThanComparator { NSPredicate(format: "timestamp < %@", $0 as NSDate) }
                GreaterThanComparator { NSPredicate(format: "timestamp > %@", $0 as NSDate) }
            }
            
            // HOW TO ADD SEARCH-BY-TAGNAME ... this does't work
            Property(\CJTagItemsAppEntity.$tagName, entityProvider: { item in
                return item.tags.first!
            }) {
                EqualToComparator {name in
                    let predicateFormat = "tagName == '\(name)'"
                    return NSPredicate(format: predicateFormat)
                }
            }
        }
        
        func entities(for identifiers: [ItemsAppEntity.ID]) async throws -> [ItemsAppEntity] {
            return []
        }
        
        func entities(matching comparators: [NSPredicate], mode: ComparatorMode, sortedBy: [EntityQuerySort<ItemsAppEntity>], limit: Int?) async throws -> [ItemsAppEntity] {
            return []
        }
    }
}

This is the Tags AppEnttiy:

struct CJTagItemsAppEntity: AppEntity {
    static var defaultQuery = CJTagItemsAppEntityQuery()
    
    var id: String
    
    @Property(title: "Name")
    var tagName: String
    
    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Test Tag")
    
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(tagName)")
    }
    
    struct CJTagItemsAppEntityQuery: EntityPropertyQuery {
        
        static var sortingOptions = SortingOptions {
            SortableBy(\CJTagItemsAppEntity.$tagName)
        }
        
        typealias ComparatorMappingType = NSPredicate
     
        static var properties = QueryProperties {
            Property(\CJTagItemsAppEntity.$tagName) {
                EqualToComparator {name in
                    let predicateFormat = "tagName == '\(name)'"
                    return NSPredicate(format: predicateFormat)
                }
            }
        }
        
        func entities(for identifiers: [CJTagItemsAppEntity.ID]) async throws -> [CJTagItemsAppEntity] {
            return []
        }
        
        func entities(matching comparators: [NSPredicate], mode: ComparatorMode, sortedBy: [EntityQuerySort<CJTagItemsAppEntity>], limit: Int?) async throws -> [CJTagItemsAppEntity] {
            return []
        }
    }

}

I haven't implemented any methods properly ... I'm just trying to get the syntax right for this.

Thanks.

@zulfishah, I took a dive into this, and there's two issues.

The first issue is that your data model for tags is referenced as an array from the item entity. That means there isn't a key path into the properties of a singular item in the array, and instead, the key paths are to the properties of an array instead.

If I were to map items to tags on a 1-to-1 basis — that is, every item entity has a property for a single tag, not an array of tags — to avoid that first issue, we then get to the more general issue, which is that nested keypath queries aren't supported for an EntityPropertyQuery.

Since you can't completely build what you're trying to accomplish, please file enhancement requests for the features you need, along with a description of your goal so we can understand your needs. If you file the enhancement requests, please post the FB number here. And if you're not familiar with how to file enhancement requests, take a look at Bug Reporting: How and Why?

—Ed Ford,  DTS Engineer

EntityPropertyQuery with property from related entity
 
 
Q