I am trying to make some conditional routes. The condition resolves on the serverside.
Route rule example:
| (dynamicRouteCT("#user" / long.caseClass[User]) ~> dynRender((page: User) => <.div("Hello, " + page.id.toString)))
.addCondition((page: User) => checkPermissions(page.id))(_ => Some(redirectToPage(Page403)(Redirect.Push)))
checkpermissions body:
def checkPermissions(id: Long) = CallbackTo.future{
/*Ajax.get(s"http://some.uri/?id=$id") map (res =>
* if (something) true
* else false
* )
*/
//the request before returns Future[XMLHttprequest] witch maps to Future[Boolean]
Future(false)
}
I got type missmatch here: (page: User) => checkPermissions(page.id)
Is it possible to perform ajax request inside conditional routes?
If we look at def addCondition(cond: Page => CallbackTo[Boolean])(condUnmet: Page => Option[Action[Page]]): Rule[Page] we can see that it requires a CallbackTo[Boolean]. Because of the nature of the JS env, there is now way to go from Future[A] to A. Although it's not a limitation from scalajs-react itself, it is an inherited reality that will affect your scalajs-react code; as this table in the doc shows, there's no way to go from a CallbackTo[Future[Boolean]] to a CallbackTo[Boolean].
This type-level restriction is actually a really good thing for user experience. The router is synchronous, it must determine how to render routes and route changes immediately. If it were allowed to be async and somehow supported Futures, then the user would experience noticable (and potentially huge) delays without any kind of visual feedback or means of interruption.
The "right way" to solve this problem is to use a model that covers the async state. This is what I would do:
Create an AsyncState[E, A] ADT with cases: Empty, AwaitingResponse, Loaded(value: A), Failed(error: E).(You can enrich these further if desired, eg. loadTime on Loaded, retry callback on Failed, timeStarted on AwaitingResponse, etc.)
Have an instance of AsyncState[Boolean] in your (local/client-side) state.
Optionally kick-off an async load on page startup.
Have the router pass its value to a component and/or check the value of this.(The router won't know the value because it's dynamic, use Callback in a for-comprehension to wire things up and satisfy the types.)
Depending on the value of AsyncState[Boolean], render something meaningful to the user. If it's AwaitingResponse, display a little spinner; if it's failed display an error and probably a retry button.
(It should also be noted that AsyncState[Boolean] shouldn't actually be Boolean as that's not very descriptive or true to the domain. It would probably be something more meaningful like AsyncState[UserAccess] or something like that.)
Hope that helps! Good luck!
Related
I have a action that looks like:
def login(...): Action[AnContent] =
customActionBuilder.async { request =>
}
Now this customActionBuilder will return either:
execute the passed in block
Future.successful(...) with a InternalServerError
So from within my action I want to pattern match against the type of result to perform some custom logic in that action e.g. if it is a InternalServerError
How can I do this?
There are different possibilities depending on what you want to do as "custom logic".
A Filter might be appropriate if you want to apply it in a global way for all Actions. You will be able to intercept the response and read it's headers (status code) to do custom logic.
You could also compose another ActionBuilder.
I'm building a PWA application, and I have used workbox registerroute for a few api endpoints, as well as an explicit service worker fetch event listener. During the debugging on some caching issues, I've noticed that these two seems to interfere with each other. Specifically sometimes the fetch handler is not triggered - which causes me trouble on debugging - I'm assuming this is due to the registerroute caching policy I have set via workbox.
My question is that, can I only pick one or the other, instead of having both fetch handler and registerroute? In my case, I needed fetch handler to deal with some advanced caching related to POST requests. So I think if I can only pick one, I'll have to stick with the fetch handler.
First, here's some background information about what happens when there's multiple fetch event handlers in the active service worker.
With that background info in mind, there are a few approaches for accomplishing what you're describing.
Option 1a: Register your own fetch event handler first
As long as you register your own fetch handler first, before any calls to Workbox's registerRoute(), it's guaranteed to have the "first shot" at responding the incoming fetch event.
The thing to keep in mind is that your own fetch handler needs to make a synchronous decision about whether or not to call event.respondWith(), and when you do call event.respondWith(), then Workbox's routes will not get used to respond to a given request.
So, you could do the following:
self.addEventListener('fetch', (event) => {
// Alternatively, check event.request.headers,
// or some other synchronous criteria.
if (event.request.url.endsWith('.json')) {
event.respondWith(customResponseLogic(event));
}
});
// Then, include any Workbox-specific routes you want.
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst()
);
// The default handler will only apply if your own
// fetch handler didn't respond.
registerDefaultHandler(new StaleWhileRevalidate());
Option 1b: Ensure Workbox routes won't match
This is similar to 1a, but the main thing is to make sure that you don't have a "catch-all" route that will match all requests, and that you don't use registerDefaultHandler().
Assuming your Workbox routes just match a specific set of well-defined criteria, and don't match any of the requests that you want to respond to in your own handler, it shouldn't matter how you order them:
// Because this will only match image requests, it doesn't
// matter if it's listed first.
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst()
);
self.addEventListener('fetch', (event) => {
// Alternatively, check event.request.headers,
// or some other synchronous criteria.
if (event.request.url.endsWith('.json')) {
event.respondWith(customResponseLogic(event));
}
});
(What's going on "under the hood" is that if there isn't a Route whose synchronous matchHandler returns a truthy value, Workbox's Router won't call event.respondWith().)
Option 2: Use custom handler logic
It should be viable to use Workbox to handle all your routing, and run your custom response generation code in either a handlerCallback (more straightforward) or a custom subclass of the Strategy base class (more reusable, but overkill for simple use cases).
The one thing to keep in mind is that if you're dealing with POST requests, you need to explicitly tell registerRoute() to respond to them, by passing in 'POST' as the (optional) third parameter.
Here's an example of how you could do this, assuming as before that you custom logic is defined in a customResponseLogic() function:
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst()
);
registerRoute(
// Swap this out for whatever criteria you need.
({url}) => url.pathname.endsWith('.json'),
// As before, this assumes that customResponseLogic()
// takes a FetchEvent and returns a Promise for a Response.
({event}) => customResponseLogic(event),
// Make sure you include 'POST' here!
'POST'
);
I have defined a route like this:
GET /login controllers.Login.showForm(continue: Option[String] = None)
Login.showForm is this:
def showForm(continue: Option[String] = None) = Action { implicit request =>
val nextPage = continue match {
case None => routes.CtrlIndex.index().absoluteURL().toString()
case Some(page) => page
}
Ok(views.html.login(nextPage))
}
Now, using action composition I made an authenticated action that performs this when the user is not authenticated:
val continue =
if (request.method == "GET") request.uri
else routes.CtrlIndex.index().absoluteURL().toString() // This is not code duplication for reasons that are out of the scope of this question.
Redirect(routes.Login.showForm(Some(continue)))
This fails to compile with this message:
too many arguments for method showForm: ()play.api.mvc.Call
[error] Redirect(routes.Login.showForm(Some(continue)))
Changing the route definition makes it work:
GET /login controllers.Login.showForm(continue: Option[String])
But then when I use the javascript reverse router, it generates the following error in the generated javascript:
SyntaxError: missing formal parameter
function(continue) {
---------^
I have tried all combination of the definition of the function signatures but when the javascript works, the other stops working or the other way around. How can I use
Optional parameter on /login
Use the reverse javascript router
Redirect to /login with the "continue" parameter
I would also like to change request.uri for something that returns an absolute path.
Thanks in advance
PS. If you see something in Spanish, let me know and I'll fix it, the code is originally in Spanish; I may have missed something even after I read it.
The likely cause is that "continue" is a reserved word in javascript
Play's javascript reverse router constructs a given route using the controller path and method argument(s) specified in the routes file; in your case, "continue" may be tripping up the js parser, much like a method named "delete", which works fine server-side but will blow up in the client-side reverse router.
This may be a non-issue for newer browsers, but have been bitten by "delete" method name on older versions of Internet Explorer (that we are required to support).
Not sure how to do this in sails.js, but I'd like to be able to, when creating a new object on the API, check to see if that object's id exists and if it does, send a 409 conflict response, and if it doesn't, create the object like normal.
For the sake of discussion, I've created a Brand model.
I'm assuming that I would override the create function in the BrandController, search for the brand based on req.param('id') and if it exists, send the error response. But I'm not sure if I'm doing this correctly, as I can't seem to get anything to work.
Anyone have ideas?
I ended up using a policy for this particular use case.
Under config/policies, I created a isRecordUnique policy:
/**
* recordIsUnique
*
* #module :: Policy
* #description :: Simple policy to check that a record is unique
*
* #docs :: http://sailsjs.org/#!documentation/policies
*
*/
module.exports = function(req, res, next) {
Brand.findOne({ id: req.body.id}).done(function (err, brand) {
if (brand) {
res.send(409);
} else {
next();
}
});
};
This allowed me to avoid overriding any CRUD functions and it seemed to fit the definition of a policy, in that only checks one thing.
To tie my policy to my create function, I modified config/policies by adding:
BrandController: {
create: 'isRecordUnique'
}
That's it. Took me way too long to figure this out, but I think it's a good approach.
Well since this is MVC you are thinking correctly that the Control should be enforcing this logic. However, as this is basic uniqueness by the primary id the model should know/understand and help enforce this.
Model should identity the conflict.
In sails the coder is responsible for the defining uniqueness, but I would have the model object do it not the controller.
The controller should route/respond by sending the view which is effectively http 409.
Yes the controller create method should be used in this case, as sails wants to provide CRUD routes for you. Assuming it is a logical create not some resultant or odd non-restful side effect.
I think of Sails.js by default providing a model controller, so use their perspective since you are using their framework. There are many approaches to Control/Model relationships.
res.view([view, options[, fn]])
Ideally the view would control the http response code, the message, any special additional headers. The view just happens to be extremely basic, but could vary in the future.
You could always set headers and response with JSON from the controller but views offer you flexibility in the future, like decoupling, the reason the MVC pattern exists. However, sails also seems to value convenience, so if it is a small app maybe directly from the controller.
My web application will be triggered from an external system. It will call one request path of my app, but uses different query parameters for different kinds of requests.
One of the parameters is the "action" that defines what is to be done. The rest of the params depend on the "action".
So I can get request params like these:
action=sayHello&user=Joe
action=newUser&name=Joe&address=xxx
action=resetPassword
...
I would like to be able to encode it similarly in the routes file for play so it does the query param based routing and as much of the validation of other parameters as possible.
What I have instead is one routing for all of these possibilities with plenty of optional parameters. The action processing it starts with a big pattern match to do dispatch and parameter validation.
Googling and checking SO just popped up plenty of samples where the params are encoded in the request path somehow, so multiple paths are routed to the same action, but I would like the opposite: one path routed to different actions.
One of my colleagues said we could have one "dispatcher" action that would just redirect based on the "action" parameter. It would be a bit more structured then the current solution, but it would not eliminate the long list of optional parameters which should be selectively passed to the next action, so I hope one knows an even better solution :-)
BTW the external system that calls my app is developed by another company and I have no influence on this design, so it's not an option to change the way how my app is triggered.
The single dispatcher action is probably the way to go, and you don't need to specify all of your optional parameters in the route. If action is always there then that's the only one you really need.
GET /someRoute controller.dispatcher(action: String)
Then in your action method you can access request.queryString to get any of the other optional parameters.
Note: I am NOT experienced Scala developer, so maybe presented snippets can be optimized... What's important for you they are valid and working.
So...
You don't need to declare every optional param in the routes file. It is great shortcut for type param's validation and best choice would be convince 'other company' to use API prepared by you... Anyway if you haven't such possibility you can also handle their requests as required.
In general: the dispatcher approach seems to be right in this place, fortunately you don't need to declare all optional params in the routes and pass it between actions/methods as they can be fetched directly from request. In PHP it can be compared to $_GET['action'] and in Java version of Play 2 controller - DynamicForm class - form().bindFromRequest.get("action").
Let's say that you have a route:
GET /dispatcher controllers.Application.dispatcher
In that case your dispatcher action (and additional methods) can look like:
def dispatcher = Action { implicit request =>
request.queryString.get("action").flatMap(_.headOption).getOrElse("invalid") match {
case "sayHello" => sayHelloMethod
case "newUser" => newUserMethod
case _ => BadRequest("Action not allowed!")
}
}
// http://localhost:9000/dispatcher?action=sayHello&name=John
def sayHelloMethod(implicit request: RequestHeader) = {
val name = request.queryString.get("name").flatMap(_.headOption).getOrElse("")
Ok("Hello " + name )
}
// http://localhost:9000/dispatcher?action=newUser&name=John+Doe&address=john#doe.com
def newUserMethod(implicit request: RequestHeader) = {
val name = request.queryString.get("name").flatMap(_.headOption).getOrElse("")
val address = request.queryString.get("address").flatMap(_.headOption).getOrElse("")
Ok("We are creating new user " + name + " with address " + address)
}
Of course you will need to validate incoming types and values 'manually', especially when actions will be operating on the DataBase, anyway biggest part of your problem you have resolved now.