-
Notifications
You must be signed in to change notification settings - Fork 0
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
Jquery adapter refactor #76
Conversation
df50f8a
to
32fe7c2
Compare
return this._ajax('DELETE', link) | ||
} | ||
|
||
_ajax (type, url, data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method has been broken up into _http
and _checkSerializeRequests
, with some of the none abstractions moved to _makeRequest
that are implementation specific
|
||
// Stringify data before any async stuff, just in case it's accidentally a mutable object (e.g. | ||
// some instrumented Vue data) | ||
if (data && ['PATCH', 'POST'].indexOf(type) > -1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
90-92 moved to _makeRequest
in the jQueryHttpAdapter
data = JSON.stringify(data) | ||
} | ||
|
||
let promise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
94-112 moved to _checkSerializeRequests
in the base HttpAdapter
@@ -522,4 +522,25 @@ describe('Store', function () { | |||
it.todo('fetches a single resource') | |||
it.todo('updates the resource with the response') | |||
}) | |||
|
|||
describe('request', function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can be moved to a separate PR if needed to reduce size
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems manageable to me. These tests seem good to include.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work. Since we're changing all this code anyway, how about running it through eslint --fix
?
this.serializeRequests = false | ||
this._outstandingRequests = new Set() | ||
this.requestInterceptor = options.requestInterceptor || (() => {}) | ||
this.responseInterceptor = options.responseInterceptor || (() => {}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could potentially make these defaults class fields; they were added to preset-env in 7.10. Of course, it would break the compiled version of backbone.store unless you update it to Babel 7 first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could do that. We might want to rebase and merge #73 if that is the route we go, as well as add documentation that the source is being consumed raw
src/http-adapter/index.js
Outdated
* @param {String} url - the url of the requwst. | ||
* @param {Object} [data] - a data payload represented as a key/value pair. | ||
* @param {string} [headers={'Accept': 'application/vnd.api+json', 'Content-Type': 'application/vnd.api+json'}] - headers to be included with the request | ||
* @param {string} [isInternal=true] - Whether the request is going to an owned/internal server, in which case header options |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I fully understand the need for both headers
and isInternal
. Even if both are necessary, it might be more transparent to have more specific params that describe exactly what they do, rather than a vague multifunctional param.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I think both are necessary since isInternal
allows for execution of the request/response interceptors (unless we want to invert that control). Maybe we should, at a minimum, document the isInternal
option in more detail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After reading over it again, I think isInternal
is unnecessary and potentially confusing. If I pass a request/response interceptor to the adapter, I would expect it to be used. Perhaps remove the empty function defaults and call them if they're defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I find it to be confusing. Likely will use requestInterceptors
as an option, but will keep the default functions on the interceptor as it is just easier to handle and likely more type safe
if (isInternal) { | ||
requestInterceptor(xhr, options) | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean that ajaxSetup
will no longer have any effect on requests made through this adapter, or does jQuery execute both implementations of beforeSend
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to verify this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it looks like the options are merged via ajaxExtend. If a 'local'beforeSend
is defined, then that hook and only that hook is used. If no 'local' beforeSend
exists, but one is defined globally via ajaxSetup
, then that hook is used. In other words, practically speaking, ajaxSetup
will no longer have any effect on requests made through this adapter in regards to beforeSend
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So your intent then is to roll this out as a new major version and have a corresponding groveco PR to reimplement the headers using requestInterceptor
?
} | ||
|
||
return `${path}/` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleanup for a future PR:
if (!type) throw `resource type is ${type}`;
let path = `${this.urlPrefix || ''}/${type}/;
if (id !== null && id !== undefined) {
path += `${id}/`;
}
return path;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would feel confident implementing those changes here, but considering the scope I am not sure it is a good idea 😅
1da863d
to
8c60e55
Compare
@racoleman I went through and addressed many of the comments. I also put a bit more diligence into the documentation. Let me know what you think! Also, for the babel stuff, we might want to hold off on that to avoid additional complexity, even though I think it would be an awesome idea to eventually implement those features |
@jessewagstaff @electrachong mind taking a look at this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a good first step to providing more http-adapter options without needing jQuery.ajax dependency.
@@ -522,4 +522,25 @@ describe('Store', function () { | |||
it.todo('fetches a single resource') | |||
it.todo('updates the resource with the response') | |||
}) | |||
|
|||
describe('request', function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems manageable to me. These tests seem good to include.
request (url, options = {}) { | ||
return this._adapter.request(url, options) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where do you see this used instead of Backbone's CRUD? Providing direct access to make http requests feels like it goes against the ideals of Backbone.Store but should help with transitioning. If something needed to make a direct request with the http-adapater could it just create a new instance of the http-adapter for itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jessewagstaff there are a few use cases where it is convenient over reinvoking a new instance of the http-adapter, as well as giving more fine grain options for a request. Within one of our grove apps, we have about 8 one-off requests, some which are in XMLHttpRequest, Fetch, or jQueryAjax. It really doesn't fit super well with the ideals of the backbone.store, but allows for a method via proxy to the http-adapter instance for convenience and ease of use. Ideally, this should help with the transition. Ultimately, once those requests are updated to use the same thing that is tested, we can repeal and replace this with something else. Baby steps 🙂 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewing just the first three commits so far. Most comments are just typo / jsdoc suggestions, hope it's not too noisy.
8c60e55
to
c4ad894
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall, just some questions to resolve about what API we want with internal requests and the request interceptors.
src/http-adapter/jquery/index.js
Outdated
type: method, | ||
headers, | ||
beforeSend (xhr, options) { | ||
if (isInternal) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would be the implications of just defaulting to always use the interceptors, or alternatively checking if they have been set to something other than the noop default callback and always using them if they were set? Put another way, what is the use case for toggling them off (via isInternal
or a hypothetical useInterceptors
) if they have already been set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bill talked to me about this via Slack,
Potentially if the requests are going to a third party, which many of them do. Or, we just dont use the request method and use something else. we dont want to be sending header data to those API as it is a security issue. We could also just use the interceptors and check the domain on the interceptor and if its not our url prefix / host, bypass it
I’m fine with either way (using useInterceptors
flag or making the app responsible for checking whether to apply the logic within the interceptors).
We could also think about whether to permit request-level interceptors and toggle adapter-level interceptors off by default for one-off requests, but I may be overengineering. I'm good with the simplest improvement and making more changes as necessary when use cases reveal themselves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bill talked to me about this via Slack,
Potentially if the requests are going to a third party, which many of them do. Or, we just dont use the request method and use something else. we dont want to be sending header data to those API as it is a security issue. We could also just use the interceptors and check the domain on the interceptor and if its not our url prefix / host, bypass it
I’m fine with either way (using
useInterceptors
flag or making the app responsible for checking whether to apply the logic within the interceptors).We could also think about whether to permit request-level interceptors and toggle adapter-level interceptors off by default for one-off requests, but I may be overengineering. I'm good with the simplest improvement and making more changes as necessary when use cases reveal themselves.
@electrachong To just keep it simple, I think making the applications responsible for this is the easiest option. This means removing isInternal
from the API. Will update shortly 🙂
@jessewagstaff @electrachong |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good. Glad the isInternal
flag wasn't needed. Left a few minor jsdoc alternatives.
}) | ||
|
||
/** | ||
* Adapter which works with data over HTTP, based on the options that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Adapter which works with data over HTTP, based on the options that | |
* This adapter works with data over HTTP, based on the options that |
* are passed to its constructor. This class is responsible for | ||
* any CRUD operations that need to be carried out with your {@link https://jsonapi.org/ JSON:API} service |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* are passed to its constructor. This class is responsible for | |
* any CRUD operations that need to be carried out with your {@link https://jsonapi.org/ JSON:API} service | |
* are passed to its constructor. This class is handles any | |
* CRUD operations that need to be carried out with your {@link https://jsonapi.org/ JSON:API} service |
} | ||
|
||
/** | ||
* Parses a JSON:API resource to a restful url. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way to phrase this is as building a url from the type and id resource identifiers. https://jsonapi.org/format/#document-resource-identifier-objects
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to bump version prior to merge?
@electrachong we do not since we will not be cutting a version immediately after merge. There are a few other PRs we still need to address / merge in. Then, a separate PR will be opened with a version bump, which will be merged in and then tagged for release. |
@AtofStryker Maybe I'm misunderstanding, can sync up via slack. These changes aren't backwards compatible because it requires the app to use the interceptors, right? Even if we install backbone.store from npm registry instead of directly from git, I'm thinking it may be better to merge to a release branch than merge breaking changes directly to master without bumping the version. That way when folks reference commit history the package version will accurately reflect the code state. Have limited familiarity with release strategies though so happy to discuss more |
@electrachong We can sync up 🙂 . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed via slack with Bill, good with approving since this seems to be how we've released new versions in the past! As I understand, we primarily install via npm by release tags and aren't too worried about individual commit history reflecting semantic versioning.
This is a revisit of the RFC and #64 and #65, a PR which is similar to this one, with some changes to the original proposal. I apologize for the gnarly diff in advance. I highly recommend stepping through the commit history to see what changed and when as it will be easier to track.
Strategy
The
HttpAdapter
has been broken out into multiple classes. These classes include theJQueryAjaxHttpAdapter
and whatever other adapter we decide to implement. This is the PR to set up the structure to implement other types of adapters.Since these adapters share common functionality, common methods have been extracted into a base
HttpAdapter
class to provide base functionality without duplication. This allows theJQueryAjaxHttpAdapter
and other adapters to implement only a few methods that are specific only to their underlying technological implementations while sharing the same public API.An example of the implementation is below. It should function the same as the previous
HttpAdapter
An example of direct request consumption is below:
Rationale
External API
request
method added to public API viaStore
->HttpAdapter
To allow for support to make generic requests that do not use the store indirectly, the decision to add a public
request
method was invoked. This allows for users to make requests directly through theStore
TheStore
proxies to theHttpAdapter
, allowing for a generic method to make any request a user sees fit.requestInterceptor
callback addedConsuming applications have a need to intercept requests before being sent. The most common use case for this is to add headers, but other options can be utilized here. Initially, this function was going to be called
OnBeforeRequest
. Due to consumer needs, we need to continue a form of interceptor behavior. This turns over configuration editing to consuming application, which is up to the discretion of the consumer to use responsibly.responseInterceptor
callback addedConsuming applications have a need to intercept response before being processed. The most common use case for this is to clean cache or perform a redirect in the case of undesirable http status codes.
Internal API
Moving
serializeRequest
andbuildUrl
to base classMoving
serializeRequest
functionality to its own function within a base class allows for the same logic to be shared by multiple adapters. This makes the logic easier to test and helps guarantee the behavior is the same between adapters. Same goes forbuildUrl
.Adding
isInternal
argument to_makeRequest
Due to some existing conditions around potential
dataType
bugs within jQuery's ajax handler,isInternal
was added to the method signature of_makeRequest
to keep existing behavior consistent. This change might not be necessary, but wanted to allow for the least change possible within the implementation to prevent potential issues.Interceptors will also be executed when isInternal
is set to true, as one off requests may/may not want to be run in those executors.Default
application/vnd.api+json
header arguments in_http
Adding
application/vnd.api+json
to the default implementation of_http
allows for all pre existing calls to_http
to behave the same, ensuring existing legacy behavior.Since this is a default argument, we leverage
request
to override the default headers implementation within_http
to allow for generic requests without predefined behavior.Common FAQ
Is the public API the same?
In short, they can be. Methods have not been removed and all methods should function the same. The only public API options added are
request
and contructor options forheaders
andaddHeadersBeforeRequest
Since there was significant refactor within the
jQueryAjaxHttpAdapter
, there is some risk involved that the default implementation did not break anything. We may want to implement a custom error class for thejQueryAjaxHttpAdapter
to make tracing Sentry easier in case problems arise.Are Backbone Collections behind the scenes using our default headers
In short, yes. Regardless of adapter and to be backwards compatible with
backbone
,ajaxSetup
will need to be used to attach these headers. TherequestInterceptor
function can likely be used just like thebeforeSend
inajaxSetup
Will the new
request
method use the interceptors?Only if the
internal
key is set totrue
inoptions