Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
API controller: support user providing a query factory
Previously, the `handle` method took in a request object, did some extra parsing and validation, built a query from the parsed + validated request, and then transformed that query with the user’s transform function. Finally, it ran the new query to get a Result, and returned an HTTPResponse created from that result. I’ve now broken this up over many separate functions (that are then optionally still called in `handle`), which is a good thing in a general/abstract sense, but also enables a specific use case. In particular, the idea is that, rather than the library constructing the initial query and then the user getting to transform that, the user should be able to control how the initial query is constructed. This may seem like a distinction without a difference, because you’d imagine that the user could take the initial query (passed to their transform function) and just discard it in the transform to replace it with whatever query they want. But the problem with that is request validation. Imagine if the user wants to handle some request like `POST /sign-in`, where the credentials are passed in an Authorization header. Even though this is a POST request, the user wants to map it to a FindQuery that’ll read the credentials and use them to look up the resulting user, or return an error. The problem is that, to invoke their query transform, the library first has to construct an initial query that it can hand the query transform as an argument. And, fundamentally, it has no idea how to construct a query for a POST request with no body and no `type` parameter, since it’s basically assuming all POST requests result in CreateQuerys or AddToRelationshipQuerys. So, the library tries to validate the request in preparation for creating such a query, and the lack of a body causes the library to throw an error, so the user’s transform never runs. So, we replace the idea of a query transform with a query factory -- a function the user can provide to build the query -- and we make a separate `APIController.makeQuery` function public, so that the user can simply compose their own function with `makeQuery` to recreate the effect of query transforms. But this reveals another problem, which is present whether the final query comes from running the user's transform function (as before) or a user factory function. Namely: when and how beforeSave/beforeRender transforms should run and generally interact with query construction. In short, `beforeSave` transforms have to run as part of/prior to constructing the query, because we have no way to take a query, once constructed, and know conceptually which parts of it were constructed with info from the request document that should be transformed. (E.g., in a bulk delete query, resource identifier objects from the document end up in the query's Criteria.) In other words, we really can't we really can't apply `beforeSave` to a query -- but only to an incoming request document, where we know what parts are resources and resource identifiers. Moreover, it might be confusing/inelegant for the query that the query factory returns to not actually be the query that runs, which is another argument for not trying to apply beforeSave to a returned query. But, in our old query transform world, applying `beforeSave` as part of query construction posed a problem: the user's code only ran after the initial query was constructed, which made it impossible for them to access the raw, untransformed data as the end user input it in the request. So, now we solve this by giving the query factory access to the untransformed request document, to read the raw data, and a function for transforming a document, so it can get the transformed dat it needs and put it in the query. That runs into another small problem, though, namely: that the `Resource` class is mutable, so running `beforeSave` produces big side effects; at the moment you do the transform, the untransformed resources that get passed to the factory are mutated too. That's really icky/not ideal, but it's tolerable: the factory function just has to read the data it needs off the untransformed doc before calling the transform function or `makeQuery`, which has to call the transform function. So, that solves how to apply `beforeSave`. Then, the question is `beforeRender`. It turns out there's an asymmetry here, because we can take a query and transform it to be the same query but with `beforeRender` applied. That's because beforeRender would be applied in `query.resulting`, and we always have a document that we're applying it to at that point. That made me tempted to take the query factory's result and wrap it in `beforeRender` automatically, as that would ensure that the user never forgets to call beforeRender on the result, which could be a security issue. However, I ultimately decided against this because: 1) I again didn't like the idea that the query returned by the the query factory isn't actually the query that's run and; 2) when the user's making their own queries (which should hopefully be rare), they already have to remember to apply beforeSave to be safe, so having to remember to apply beforeRender (which will be automatic if they're using `makeQuery` as a base and composing the `query.returning` function it generates) seemed like not too much extra burden. Note: a big nice outcome of this commit is that all the library’s built-in query construction logic is unit testable, because APIController.makeQuery is a pure function. Before we’d reified the idea of the query and broken construction apart from running, we’d have needed to do an integration test where we spied on adapter methods. This commit also includes another of other changes; see UPGRADING.md diff in this commit.
- Loading branch information