Refactor Approach to SuperTokens
Inputs and Outputs to Be More Flexible
#5
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Refactor Approach to
SuperTokens
Inputs and Outputs(This PR has only been created for historical purposes. This branch should be merged locally.)
The
SuperTokensInput
andSuperTokensOutput
utility classes are intended to provide ideas to theSuperTokens
team regarding how they can update their functions to be more flexible. This flexibility will enable their functions to be used across various SSR frameworks with ease. These utility classes represent the inputs which these functions should accept (if any are needed) and the outputs which these functions should return (if any are needed), respectively. As potential improvements to whatSuperTokens
could use for inputs and outputs are discovered, so we ought to update the utility classes that we have here in this repo. Thus, we're making our first refactor here.Changes
Expose
SuperTokens
's Response Cookies ExplicitlyAfter chatting with Rich Harris (creator of Svelte) on sveltejs/kit#8409, it seems that it's better to expose any necessary cookies explicitly rather than wrap them in
Headers
. The short reasoning for this is that it provides more options to developers using SSR frameworks. (The previous implementation locked some developers out of usingSuperTokens
in the framework of their choice -- for instance, inSvelte Kit
.)We've chosen to expose the output cookies as a
Map
, where the key is the cookie'sname
(as astring
) and the value is the entire cookievalue
(as astring
). This means that the value returned fromMap.get
includesname=value
and all the cookie's attributes.Why Expose the Response Cookie
value
As astring
Instead of anobject
?Exposing the entire cookie
value
as astring
is arguably the best mix between versatility and ease of use. Most developers will simply be calling something likeheaders.append("Set-Cookie", cookieValueString)
or something similar to meet their needs. This is a nice and simple developer experience. And because the cookievalue
string is standardized, developers can reliably convert the string into an object if such a conversion is necessary for their SSR framework.Exposing the cookie value as an object is not as reliable as it seems. First of all, there is no standard object representation of cookies. (Even the popular
cookie
npm package does not use a consistent object representation between itscookie.serialize
andcookie.parse
helper functions.) This means that even if we exposed the cookie value as an object, the end-user would still potentially need to convert that object into another object representation. This, of course, is bothersome -- especially if the object key mapping is convoluted.Second, exposing the cookie value as an object would require many developers to convert the object to a string (for their response headers). And any time
SuperTokens
changes this object representation (for instance, to match the most popular object representation of cookies in the future), it would result in a breaking change.Since cookie values will only be interacted with one-at-a-time, it is sufficient for developers to derive each individual cookie's object representation (if one is needed) from the string value based on the location of the
;
and=
characters.Why Use
Map<string, string>
for the Response Cookies Instead ofstring[]
?Storing the cookies in a
Map
provides more options to the developer than storing the cookies in anArray
. Like anArray
, aMap
is stillIterable
, and useful helpers likeforEach
can be used reliably with it. Beyond this, theMap
can be converted to anArray
withArray.from
if necessary. But unlike anArray
, aMap
provides developers (andSuperTokens
) with a simple, reliable way to get (or set or reset) a given cookie byname
. Attempting to do this with anArray
would require much more effort.A
Map
was also chosen over a regular object sinceMap
s are inherently iterable.Expose Reponse Headers As a
Map<string, string | string[]>
The
responseHeaders
are now represented as aMap
, where the key is the header'sname
(as astring
in the exact casing of the HTTP header), and the value is either astring
for a single value or astring[]
for a header with multiple values (i.e., a header that should be returned in the response multiple times).When leveraging this
Map
to set the headers of a trueHeaders
object, developers will need to do something like the following:To reduce code duplication, developers can abstract this logic into a helper function.
Why Use
Map<string, string | string[]>
for the Response Headers Instead ofHeaders
?Unfortunately, the standard Web Headers API is not reliably supported in Node.js at the moment. Moreover, not every SSR framework is guaranteed to support this representation of headers to begin with. Thus, at the moment, it's simpler and more reliable to expose a common iterable object that all Node.js applications can use with relative ease. And the iterable object that's closest to
Headers
is arguably theMap
.Moreover, our new representation is compatible with Node.js out of the box. The native
response.setHeader
method expects a string for headers that should be set only once and an array for headers that should be set multiple times in the response. Thus, developers using the native features of Node.js could simply do the following:Short and sweet. 😄
Require the Request Headers As a
Map<string, string>
, and Expect the Incoming Headers to Include the CookiesThe meaning of this title is fairly straightforward. No explanation is necessary.
Why Use
Map<string, string>
for the Request Headers?We've already addressed why a
Map
is better thanHeaders
for Node.js. So the greater question here is: "Why require a headers input with a different shape than our own headers output?"The main idea here is ease of use. We want the outputs that we return to be as clear, flexible, and easy to use as possible. Similarly, we want the inputs that we require to minimize the effort on the developer's end as much as possible. And more than likely, the incoming request headers will have the following representation:
If we run with these assumptions (which are fairly reliable), then
SuperTokens
can easily derive the header values on its own, and the developer hardly has to do any work.For developers whose headers are represented by the
Headers
object, they can simply do something like the following:Developers using the native features of Node.js would need to do something like the following:
See the Node.js documentation for more information regarding what the
IncomingMessage.headers
field looks like. Note that the Web StandardHeaders.get
method is case-insensitive. In fact,new Map(instanceOfHeadersClass)
will automatically normalize all header names by making them lowercase. So as long as theSuperTokens
implementation assumes that all incoming request header names are all lowercased, everything will work fine without any additional effort from the developer.If someone can sufficiently argue that consistency between the shape of the input headers and the shape of the output headers is a better developer experience than the ease of use we've just mentioned, then we can revisit this decision. However, for now, this approach seems to make sense.
Why Not Require the Input Data to Include a Separate Cookies Object?
According to Wikipedia:
This means that we can reliably derive all the request cookies from the request headers. There is, therefore, no reason to require the developer to go through the extra effort of providing cookies separately.
If someone can sufficently argue that consistently requiring cookies for BOTH the input AND the output is a better developer experience than the ease of use we've just mentioned, then we can revisit this decision. However, for now, this approach seems to make sense.