-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove infix operators in filter syntax? #172
Comments
Hearing no objections, I'm gonna go ahead and make this change, and publish a new major version of @json-api/query-parser soon. If you want the old behavior, you can explicitly install v2 of the query parser library ( import { parseSort, parseFilter } from "@json-api/query-parser";
// Use old parsers
const API = new API.controllers.API(registry, {
sortParser(legalSortOperators, rawQuery) {
const paramValue = getQueryParamValue("sort", rawQuery);
return paramValue && parseSort(paramValue, legalSortOperators);
},
filterParser(legalFilterOperators, rawQuery) {
const paramValue = getQueryParamValue("filter", rawQuery);
return paramValue && parseFilter(paramValue, legalFilterOperators);
}
}); Note: the code above depends on a function getQueryParamValue(paramName: string, queryString: string | undefined) {
if(typeof queryString === 'undefined') { return undefined; }
return queryString.split("&").reduce((acc: string | undefined, paramString) => {
const [rawKey, rawValue] = splitSingleQueryParamString(paramString);
return rawKey === paramName ? rawValue : acc;
}, undefined);
};
function splitSingleQueryParamString(paramString: string) {
const bracketEqualsPos = paramString.indexOf("]=");
const delimiterPos =
bracketEqualsPos === -1 ? paramString.indexOf("=") : bracketEqualsPos + 1;
// returning [undecoded key, undecoded value]
return delimiterPos === -1
? [paramString, ""]
: [paramString.slice(0, delimiterPos), paramString.slice(delimiterPos + 1)];
} |
Thinking about this more, I'm realizing that the problem with the current filter syntax is, more generally, that the special cases around where/if the operator is listed, which are so nice for usability when writing the filter constraints by hand, can lead to pretty problematic bugs when generating the filter constraints programatically. E.g., imagine an n-ary operator that takes a one or two args, some of which can be identifiers/field references, as in The All this makes me more convinced than ever that the operator needs to be in a consistent spot. The obvious answer is to just always put the operator first, as I proposed in the OP, but, after sitting with that, I really don't like how it makes the by-far most common cases (binary operators) harder to read. At least to me, So, my latest thinking is that:
Overall, I think I like this approach, and plan to implement it now. Some breaking change was necessary to resolve the security issues of the current syntax, and some change beyond "always put the operator first" was needed if we wanted to be able to safely keep the |
Note: my comment above has been heavily edited and describes the resolution to this issue. |
See ethanresnick/json-api#172 (comment) 66 for an overview of the changes
See #172 (comment) 66 for details/context.
This has been addressed in 3.0.0-rc.6. See 0c0b35f |
@carlbennettnz FYI, there's now a filter/sort param serialization function in the |
Awesome, thanks! |
This is an open question for current json-api users:
Would it be worth it to remove the distinction in the current filter syntax in which binary operators get their operator "infixed" (i.e., placed in the middle, like
(x,gte,4)
) but other operators get their operators placed at the beginning (e.g.,(and,(...),(...))
)? Instead, the operator would always be the first item.The advantages of this are:
It becomes easy for a binary operator to add extra, optional operands down the line. E.g., imagine you want to extend the
lte
operator to support "chained comparisons". So, instead of saying(and,(0,lte,minDistance),(minDistance,lte,maxDistance))
, you want to support something like(lte,0,minDistance,maxDistance)
. In the current setup, supporting this would change where the parser is expectinglte
operator to be, because it's no longer a binary a binary operator. So, either you couldn't change it (and would create a new operator instead), or the change would break existing users, or you'd have to complicate the parsing and serialization logic further to support both forms (i.e.,(x,lte,y)
and(lte,x,y)
).Related to the above, the parser and serializer (in progress) already have to special case binary operators. In this simplification, those special cases could be removed, and operators also wouldn't have to be defined with whether they're binary (though perhaps they still could be defined with their arity to enable automatic validation of the length of the arguments list).
Forcing the infixing of binary operators was intended to offer better usability, and it probably does on the whole. However, the forced infixing sometimes leads to really unreadable results. E.g., there's an operator that takes a center point and a radius and returns a circle value that can be used as an operand to the
geoWithin
operator (to find resource objects inside that circle). Because this operator is binary, it has to be infixed, so you end up with([0,0],toGeoCircle,100)
. That's pretty cryptic. (The first arg is the center and the second is the radius.) It'd probably be clearer to have(geoCircle,[0,0],100)
, as that reads more like a standard function call.The disadvantages are:
This is a break from the existing format. Big mitigating factor: people could continue to use the current version of the parser package while still updating the json-api lib to the latest version — which is very nice!
Most binary operators, which are the most common case, probably get a bit less readable, if you accept that that people find it easier to read
(a,gte,b)
than(gte,a,b)
.The text was updated successfully, but these errors were encountered: