loreanvictor / tyfon

typed functions over network
https://loreanvictor.github.io/tyfon
MIT License
37 stars 4 forks source link

support input argument pre/post processing with access to http request context #2

Closed loreanvictor closed 3 years ago

loreanvictor commented 3 years ago

it would be pretty great if arguments could be somehow automatically pre-processed / post-processed when transferred between server and client, specifically if access to request context was provided during this pre/post processing. this would basically allow middlewares while preserving the idea of masking the network layer completely from the user.

inspiring usage examples:

this could be achieved by sets of factory / parser functions, or it could be achieved using classes. the later would be more convenient and seamless looking overall, but would be an exception to "all argument / return types must be plain js objects" as it would become confusing to determine which classes are allowed and which are not. it could also be achieved by special keys in the objects which could store the pre/post processor functions.

michie1 commented 3 years ago

Using this for providing the context does not have my preference, but it has the advantage that it is easy to use and easy to not use, the arguments won't change.

You could have a specific argument dedicated to reading/updating the context, but this one should only be available on the server side. For me that's not a problem because most of my endpoints/functions would be using some part of the context and even if you don't use it, you only have an unused argument, which is also not a big problem. (With express middlewares you have the same problem, req is not always used, but you need to declare it in the callback.)

You could also put the context argument at the end and make it optional.

loreanvictor commented 3 years ago

this was also my first thought as the gateway for accessing request context. However, I have the following issues with it:

  1. It will break the symmetry of the function type on the server and the client (different this type). This also necessitates additional work on client-side code as this type asymmetry would need to be handled during SDK generation.
  2. It either results in passing this around awkwardly within the function or exposing a lot of network-aware code inside it:
export async function myFunc(this: ContextAwareThis, ...) {

  // 👉 most probably we need to extract some data from various segments of
  // the request such as the header, and thats why we need access to request context.
  const whatIWant1 = extractor1(this);
  const whatIWant2 = extractor2(this);
}
export async function myFunc(this: ContextAwareThis, ...) {

  let whatIWant1 = undefined;
  if (this.request.headers...) { /* super request-aware code */ }

  let whatIWant2 = undefined;
  if (this.request.body...) { /* super request-aware code */ }
}
  1. This solution is actually incomplete, as it only addresses a method for extracting extra information from request context, leaving the issue of injecting that extra information on the client-side unaddressed. As with this solution on the server-side code there is network-aware code exposed inside the function, I expect potential solutions for client-side will similarly do so, further diverging from the goal of masking network layer.

The problems with this generally apply to having optional / trailing arguments providing access to the context, as basically this can also be thought of as just an initial (and a little bit more masked) argument.



Alternative

A method of masking the network layer code from the function would be to do the extraction before hand, by somehow knowing what kind of extraction does the function need. This can be achieved by some arguments being somehow marked for particular extraction, which the TyFON network layer could then run before passing them to the function:

export async function myFunc(whatIWant1: Type1, whatIWant2: Type2) {
  // network agnostic code
}

This also provides a solution for network-agnostic injection on the client side:

// 👉 Type1 and Type2 do not need to necessarily be classes,
// I am just using class signature for clarity here.
await myFunc(new Type1(...), new Type2(...));

The process would be like this:

Example:

//
// --> AuthToken injection injects Bearer in request header and extraction reads it
// --> UploadFile injection sets content type and adds uploaded files to request body,
//     while the extraction reads the file content.
//
export async function uploadPicture(auth: AuthToken, user: User, picture: UploadFile) {
 // ...
}

Pros:

Cons:

Open Issues: