-
-
Notifications
You must be signed in to change notification settings - Fork 7
Mongo JSON utility #2
Comments
Inspirtation can be found in GQL: https://github.com/TryGhost/GQL/blob/master/lib/lodash-stmt.js |
While doing research for these filters came out to a limitation of NQL api. After default/custom fields were applied to a filter object self._filters, there's no way to actually use these filter when extending knex'es QueryBuilder object here. When we substitue GQL with NQL this code:
would become:
but at the moment NQL's querySQL only accepts single parameter and uses the filter string that it was initialized with. |
@gargol Yeah. We need to design this 👍I am wondering why Ghost combines (mongo) JSON and not filters as strings 🤔
|
It would be hard to achieve appropriate merging of default/enforced/custom filters on a string level hence the need to manipulate already parsed objects |
What do you mean by that? |
We have three use cases in Ghost:
I am already getting a mongo JSON when i parse a filter. That's why i am wondering if it's easier to combine filter strings. BTW: Did you create an issue for researching existing mongo json utility (merge/reject mongo json objects)? |
Haven't created a specific issue as there's no real need so far. Regular merging with
|
It's impossible to detect which filters overlap with enforced ones for example this would be rather hard to do with strings only - https://github.com/TryGhost/Ghost/blob/master/core/test/unit/models/plugins/filter_spec.js#L415 |
Using lodash does not work. It only works if you want to merge JSON objects. As soon as i pass a custom filter
This is a default use case. I don't say we have to parse strings 🙊, but i'd suggest to collect in an issue:
|
|
Sorry to be the bearer of bad news. As I mentioned might be the case, we definitely cannot go live with a regex parser in place of this utility. At a fundamental level, the concept of using regex to parse a language we already have a proper lexer & parser for is pretty darned dirty. At a conceptual level, any manipulation of NQL should really be happening at the JSON level, or as part of the parser itself. NQL-Lang is a language for humans, Mongo JSON is a language for machines. We parse NQL-Lang into Mongo JSON so that it's easier to work with programatically. We parse, rather than using regex, because NQL-Lang is optimised for humans, flexible, and is a complete language on its own. Regex cannot do a good job of parsing it. Concating strings was definitely a way to get us a first-pass implementation to focus on other aspects, but it has severe limitations:
To be clear, I had concerns about the approach, but it's actually far easier to exploit this than I even imagined. I don't like coming at you with nothing but problems, but I wanted to get this written down as I'm already a little delayed getting around to testing. I will try a couple of implementation ideas I have now and update this issue ASAP. |
@ErisDS would passing the query string through NQL parser as a first stage validation and improving current set of regexes be enough of the improvement for now? Just thinking out loud about the solutions that are less work intensive to be able to meet our target goals 🤔 I'll get to a more in-depth analysis of this issue, first thing tomorrow. |
As far as I am aware, it is categorically not possible to solve this problem safely with Regex. It seems like one of two fundamental pieces of information are missing here: first that NQL supports arbitrary nesting, and second that arbitrary nesting is usually accepted as the cut off point for regex. https://stackoverflow.com/questions/11905506/regular-expression-vs-string-parsing |
Was trying to find a good argument//examine te problem by coveing the holes listed above. Was able to easily cover up the first case by running the filter through NQL lexer (didn't catch any errors for the second case). But the last point (as it is a vliad NQL) made it rather clear that not properly parsing and matching on the keys themselves is just too dirty... I can make up a regex that will match similar cases but then why do all the work making up unreadable regexp code 🤷♂️ Current solution seemed good enough for the limited usecases we have, but going forward it's definitelly better to invest time in proper Mongo JSON util. @ErisDS did you have time to:
|
So far, I've only had chance to re-familiarise myself with the existing code and use cases, so that I can understand what problems we really need to solve, and whether any of the approaches are viable. Conceptual approaches:
Use cases: There are 3 types of filter, in terms of priority / levels:
The rules here are a kind of cascade. The cascade is how it works now:
Therefore, the existing approach involves both an iterator and naive logic. I was wondering if logically speaking, defaults should work with outer nested OR groups 🤔* as a way to reduce the workload*. However, yu have to iterate & reduce the level 2 statements first anyway for enforced logic to work. To go through this in a quick natural example:
The result must be Potential quick solution without iteration and reducing: If you use outer nested logical groups without reducing, you end up with:
This isn't insecure - but it does result in no results. Maybe that should be expected behaviour for a "bad" filter like this, however it would be a potential breaking change to suddenly stop ignoring bad filters. A variation on this theme might be to validate and reject filters that conflict with enforced filters, but again, we fall into making breaking changes. Having looked quickly at the parser code, I don't see an obvious quick path to a solution there. This leaves us with needing to try to implement an iterator, I think. I've written this up fairly quickly. Let me know if there's anything here that doesn't make sense. 🤔* This is unquestionably wrong LOL |
As discussed, we agreed on going for Naive merging of Mongo JSON. The current filter merging test suite should be expanded with cases from GQL util |
Poked around with util implementation based on mongo json, and some initial work can be found here - kirrg001/Ghost@replace-gql...gargol:mongo-json-utils. Currently took a very naive approach for filter reduction just to see how things work together. One of the changes that need to be done in NQL though is accepting full mongo json object instead of nql string. Can see already that there will be small challenges reducing filters that are nested under multiple groups like $or:[]. $and:[], but that just needs a bit more thinking through. Also the |
Can you walk me through this assertion please? |
At the moment NQL only accepts a query string and options object, which we used this way on the branch where we are substituting GQL. Now that we decided to merge filters as Mongo JSON objects, we will no longer have a string available ( |
We control NQL entirely, but I feel like there are a bunch of assumptions about the API for NQL here 😬 I think you're suggesting the API be something like
But that's by far and away not the only option! Also note that the const RELATIONS there is actually, options with a What's to prevent NQL from understanding the concept of "default" and "override" much the same as nconf does? 🤔 |
Yes, the example above is exactly what I've meant. In our case, a fuller picture is something like this: // step 1
filter = nql.util.merge({enforced, custom, extra, default}) // at this point filter becomes an Mongo JSON object
// step 2
filter = nql.util.replace(filter, aliases) // the name 'aliases' might be a bit confusing, maybe 'expansions' would explain better what it actually does, but this has to operate on Mongo JSON object as well and 'expand' keys defined in the config into statements
// tada
nql(filter, options).querySQL(qb); Ideally the logic from // this can be same concatination that is done on replace-gql branch
// https://github.com/TryGhost/Ghost/pull/10159/files#diff-f359249f88688ade047760ef9bca4f6bR110
filter = `${custom}+${extra}`
options = Object.assign(options, {filters: {enforced, default})
nql(filter, options)... Similar should happen with logic from options = Object.assign(options, {expansions: ALIASES}) // following current vairable naming With this approach the interface doesn't change at all. But currently I'm just looking for a way to tell nql "take this pre-processed mongo json object and build up sql upon that". This is needed to be able to test current utilities work with all the test suites in Ghost and be able to change things in case they don't without going back and forth doing updates in nql package. Rewrote this message about 1000 times, hopefully it's clear 😃 |
Ah, this makes more sense! |
Aliases to me are simple table name aliases, completely agree with renaming this concept to expansions 👍 |
To better track the progress of utilities replacement created a PR to @kirrg001's branch with replace-gql code - kirrg001/Ghost#1 |
Initial go for mongo json utils merged into running |
As current implementation of utilities has passed through initial testing and 100% works with functional test coverage in Ghost the next step is moving it to nql. As discussed above, there could be multiple approaches to pass the query information to nql. The one that looks optimal to me would stay with current interface and expand what's being possible to pass in through options object. In Ghost the code would looke like:
This approach leaves Ghost with only knowledge of nql queries (no tiyng up with with mongo-knex) and the interface of nql only changes in the options object. |
Looks good in general 👍 Two things i have spotted
Naming suggestion: The
Isomehow find it weird that we pass a single filter as first argument Futhermore, it's sad that we still need to support this "extra" filter, which is a result of some older API query options (e.g. ?status=). An alternative approach could be to allow either passing a single filter as string or an array of filter strings. The order in the array defines the priority. By that we don't need to tell NQL at all which filters we have.
That should not break the API and is IMO more flexible? 🤔 |
@ErisDS, believe we'd also need a new repo in NexesJS for |
@kirrg001 regarding 1st point, is this the proposed form? :
To me the name of the key isn't 100% suitable tbh, because it's not just a filter but something that the original key has to be glued together with. On the other hand, when naming it as suggest, it lets us know that it's an 'nql string' that is being passed and not a vague 'expansion' thing 🤔 ... maybe I'd go one step further and apply the comment I left in the proposal:
so it would become:
Regarding 2nd point. Don't think passing an array would be possible, because then we loose the priority of filters. For example we have combine ->
but that's just moving stuff from options to main parameter which I think is fine. |
Simpler is better, how about
Maybe just |
@ErisDS |
@ErisDS do you have any specific preference around nql interface as discussed above:
vs
vs
|
With expansions defined as something separate, what's the use case for With simplicity and ref material in mind, I'd use
Assuming lots of queries in a similar env...
Or
Built whatever feels comfortable with how we're using it in Ghost for now, but I can imagine all of these being useful. |
The Expansions don't help in this situation as there's quite a lot of logic involved. What we could do to simplify things, is combining custom and extra filters before passing to |
We don't do multiple queries on the same nql instance in Ghost atm. Don't think we'd benefit right away from having a separate contructor. So, this approach should be sufficient for current use cases:
But still need to figure out where the @kirrg001 what are your throughs? |
@gargol Just go with any suggested solution, which works for now 👍 |
closes #1 - combineFilters - findStatement - rejectStatements - expandFilters
closes TryGhost/NQL-old#2, refs TryGhost/mongo-utils#4 - Extended `nql.parse` with new overrides & defaults option parameters - Forwarded merge action to mongo-utils - Added test coverage - Improved readability
Requirement
NQL should offer a new interface utility to handle multiple filters.
Currently, you can only use NQL like this:
nql('filter=a+b'). querySQL()
(see)You can pass a filter and the NQL interface will parse this filter into mongo JSON and then passes mongo JSON into mongo-knex.
But what if you want to combine multiple filters? It would be great if NQL could offer a utility set.
e.g. i have two filters
filter=a:b
(custom)filter=a:c
(enforced)IMO the challenge here is to combine mongo JSON.
Tasks
options.context.public ? 'status:published' : null;
options.context.public ? 'status:[inactive, locked]' : null
options.context.public ? 'page:false' : 'page:false+status:published'
options.context.public ? null : 'status:[inactive, locked]'
The text was updated successfully, but these errors were encountered: