Breeze / breeze-client

Breeze for JavaScript clients
MIT License
38 stars 16 forks source link

BUG: Predicate query fails when searching for a text word that is the same as an Entity name. #73

Open EchoFoxxtrot opened 1 year ago

EchoFoxxtrot commented 1 year ago

Consider the following scenario:

I have an entity type called Area that corresponds to an Area table in my database and I have another entity type in the same entity metadata set that contains a Title field, defined as a string, that I want to query against by creating a Predicate something like the following:

let searchFilter = 'area' // simulating typed input from a search criteria entered by a user through the UI
let myNewPredicate = new Predicate('Title', FilterQueryOp.startsWith, searchFilter);

I then use my predicate with the EntityQuery.where() method to query my other table which DOES NOT have a field or navigation property called "Area".

The result is that I will receive an error from the EntityManager that states:

Error: unable to locate property: Area on entityType:
<MyOtherEntityClass>#<My.DataAccessModel.Namespace>

This is true when you write any Predicate using any valid FilterQueryOp operator to query for any string that is an exact caseless match of the name of any Entity Type that is registered from a server Metadata call on an Entity Framework Core dbContext.

My current workaround to this is to remove the last character of a "startsWith" predicate or the first character of an "endsWith" predicate, but it gets dicey when I need to use an "equals" predicate, so it would be nice if this bug could be fixed.

steveschmitt commented 1 year ago

Not a bug.

In this case, you need to use some additional syntax to indicate whether you are querying the literal value or the property.

// query where the Title property starts with the Area property
let propertyPredicate = new Predicate('Title', FilterQueryOp.startsWith, { value: 'area', isProperty: true });

// query where the Title property starts with the literal 'area'
let valuePredicate = new Predicate('Title', FilterQueryOp.startsWith, { value: 'area', isProperty: false });

This is in the Breeze documentation, but it's buried pretty deep.

Will-at-FreedomDev commented 1 year ago

I attempted to fix this issue in my own application by using isProperty: false, but now my queries that are not string based queries are all failing. I also tried isLiteral: true but that had the same effect.

steveschmitt commented 1 year ago

Can you give me an example of a query that is failing, and what the error is?

Will-at-FreedomDev commented 1 year ago

Sorry, I've already made a workaround: to test the value I wanted to query by and only apply isProperty: false when the value is a string type and not a date formatted string. This fixed the data type errors we were seeing (could not filter Int32 by Double, etc.) For background, all the data tables uses a single architecture to send these queries to our back-end. A user happened to filter a string column by a value that happened to also be a property name on the same entity. This causes issues when both are strings and the behaviour is not expected. The other and worse thing that happened was if you searched by a property name that was a different data type completely (string could not be filtered by Int32 is the sort of error we saw).

I was hoping isProperty: false simply disabled making the query search by another property in the filter, but it had other unwanted side-effects aforementioned.

Will-at-FreedomDev commented 1 year ago

Sorry I don't have more expertise. I've just taken over this project and never heard of Breeze :) In code, my workaround was to do this:


// don't accidentally attempt to query by another property name because it's ambiguous behavior
            // if the value is a string and not a date formatted string, then set isProperty: false
            // we can't always set isProperty: false because the query sends the wrong data type to the server for some reason.
            let queryValue = typeof value !== "string" || /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d*)?Z?$/.test(value)  ? value : { value, isProperty: false };
...
let result = new Predicate(_this.fixFieldName(criteria[0]), translateBinaryOperator(operator), queryValue);
steveschmitt commented 1 year ago

Please help me, because I'm trying to reproduce the scenario in the Breeze tests.

Let's say I have an entity with three properties, stringProp, intProp, and dateProp, which are type string, Int32, and DateTime respectively. What do I do to get the "could not filter Int32 by Double" and "string could not be filtered by Int32" errors?

Also, what version of Breeze are you using on the client and the server?

Will-at-FreedomDev commented 1 year ago

The server is using Breeze.AspNetCore.NetCore (6.0.4) and the client is using breeze-client: ^2.1.2

In my scenario this Predicate caused the error I mentioned:

new Predicate("intProp", "eq", { value: 1, isProperty: false }); // it was passing Data Type: Double to the server instead of `Int32` or whatever it should have been passign

This worked fine

new Predicate("intProp", "eq", 1);

Something about setting that flag caused the query to be built incorrectly. As I mentioned, I have little to no experience with Breeze, just enough to make a workaround for the filtering by property name issue the OP mentioned (I understand there are circumstances where it would be appropriate to have that behavior).

If you can't recreate it in your tests, I'm sure something in the architecture of this project is to blame. Thank you for being a good steward of this project.

Will-at-FreedomDev commented 1 year ago

The "string couldn't be filtered by Int32" error was caused by this predicate:

new Predicate("stringProp", "eq", "intProp");

This fixed that issue (because I didn't want to filter by the actual property value, I wanted to filter by the literal string....

new Predicate("stringProp", "eq", { value: "intProp", isProperty: false });

I was hoping I could just always set isProperty: false but as mentioned, that had side-effects in my project

steveschmitt commented 1 year ago

Thanks for the help. I'll see if I can fix it. I'm glad you found the workaround for you.

steveschmitt commented 1 year ago

I could not reproduce the problem.

The data type of a property -- String, Int32, Double, etc. -- comes from the Breeze metadata, which is consumed by the Breeze EntityManager and usually generated based on the server classes. Your metadata may need updating or correcting.

You can also specify the data type explicitly in the predicate:

new Predicate("intProp", "eq", { value: 1, isProperty: false, dataType: "Int32" });