I got a basic question.
I'm trying out Sails (http://sailsjs.org/) and it has terminal commands to generate entity such as User entity:
sails generate api user
My question is, the UserController.js file shows:
/**
* UserController
*
* #description :: Server-side logic for managing users
* #help :: See http://sailsjs.org/#!/documentation/concepts/Controllers
*/
module.exports = {
};
How come when I access:
http://localhost:1337/user/create
It knows how to create a new User entity ? The controller clearly does not have a create action like this:
module.exports = {
create: function(req, res) {
// code to create new user
}
};
So surely nothing should happen.
I did a bit of Symphony 2.0 PHP web framework and we needed to create those actions manually.
I'm confuzzled and impressed at the same time, any ideas ?
Welcome to the Sails.js world!
You have just discovered the Blueprint API.
When you lift your app, Sails will add generic actions to your controllers that have a model of the same name (to this day find, findOne, create, update, destroy, populate, add and remove actions exist implicitly). That's called Blueprints actions.
In addition, Blueprints routes can also be binded to your controllers' actions. Here is the list of those routes:
Blueprints RESTful routes: automatically generated routes to expose a conventionnal REST API on top of find, create, update, and destroy actions
GET /post -> PostController.find
GET /post/:id -> PostController.findOne
POST /post -> PostController.create
PUT /post/:id -> PostController.update
DELETE /post/:id -> PostController.destroy
Blueprints shortcuts routes: simple helpers to provide access to a controller's CRUD methods from your browser's URL bar
GET /user/create?name=joe -> Post.create
GET /user/update/1?name=mike -> Post.update
GET /user/destroy/1 -> Post.destroy
Blueprints actions routes: automatically create routes for your custom controller actions
GET /group/count -> Post.count
Each of them can be deactivated in the config/blueprints.js file.
You can find more details on the docs.
Check this SO question if you want to redefine the blueprints actions.
Related
We have an ASP.NET Core 5 Rest API where we have used a pretty simple route:
[Route("api/[controller]")]
The backend is multi-tenant, but tenant-selection has been handled by user credentials.
Now we wish to add the tenant to the path:
[Route("api/{tenant}/{subtenant}/[controller]")]
This makes cross-tenant queries simpler for tools like Excel / PowerQuery, which unfortunately tend to store credentials per url
The problem is to redirect all existing calls to the old route, to the new. We can assume that the missing pieces are available in the credentials (user-id is on form 'tenant/subtenant/username')
I had hope to simply intercept the route-parsing and fill in the tenant/subtenant route values, but have had not luck so far.
The closes thing so far is to have two Route-attributes, but that unfortunately messes up our Swagger documentation; every method will appear with and without the tenant path
If you want to transparently change the incoming path on a request, you can add a middleware to set Path to a new value, for example:
app.Use(async (context,next) =>
{
var newPath = // Logic to determine new path
// Rewrite and continue processing
context.Request.Path = newPath;
await next();
});
This should be placed in the pipeline after you can determine the tenant and before the routing happens.
I have an existing web application accessing a MySQL database. I'm porting this application to Symfony. The new application has to use the old database, as we cannot port the whole application at once, i.e. the old and the new application are accessing the same database and the applications are running simultaneously.
The old application had a simple CMS functionality which has to be ported:
There is a table pagewhich represents a page tree. Every page has a slug field. The URL path consists of those slugs representing the path identifying the page node, e.g. "/[parent-slug]/[child-slug]".
The page table also contains a content field. As I already mentioned, the CMS functionality is very simple, so the content is just rendered as page content inside a page layout. The page entry also specifies the page layout / template.
My problem is that I don't know how to set up the routing. In a normal Symfony application I'd know the URL patterns before, but in this case they are dynamic. Also routes cannot be cached, because they could be changed any time by the user. I wonder if I have to drop Symfony's routing completely and implement something on my own. But how?
Now I found Symfony CMF which tells a lot about the framework VS CMS routing conflict. So first, I thought this would be the right way. However the tutorials aim at building an entirely new application based on PHPRC. I wasn't able to derive the tutorial's concepts to my use case.
since you run several URL rules on one symfony application, you will need to work with url prefixes. Either your cms should work with a prefix /cms/parent-slug/child-slug or all other controllers. Otherwise you are not able to differ which controller is meant when a dynamic request arrives.
You can try a workaround with a KernelControllerListener. He will catch up every request and then check if a cms page is requested. On the basis of the request you can set controller and action by yourself. Concept:
Create only one route with "/". Abandon oll other rules. Then create a Listener like this:
<?php
namespace AppBundle\Listener;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
/**
* Class KernelControllerListener
* #package ApiBundle\Listener
*/
class KernelControllerListener
{
/**
* #var CmsRepository
*/
private $requestParser;
/**
* KernelControllerListener constructor.
* #param CmsRepository $CmsRepository
*/
public function __construct(CmsRepository $CmsRepository)
{
$this->CmsRepository = $CmsRepository;
}
/**
* #param FilterControllerEvent $event
*/
public function onKernelController(FilterControllerEvent $event){
$request = $event->getRequest();
//should be /parent-slug/children/slug or any other path
$path = $request->getPathInfo();
if($this->CmsRepository->getCmsControllerIfMatch($path)){
//cms repository search in db for page with this path, otherwise return false
$event->setController([AppBundle\CmsController::class, 'cmsAction']);
return;
}
//repeat if clause for any other application part
}
}
in services.yml:
app.controller_listener:
class: AppBundle\Listener\KernelControllerListener
arguments:
- "#app.cms_repository"
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Edit: catch all routes, see https://www.jverdeyen.be/symfony2/symfony-catch-all-route/
The question is: Do you whant to migrate the data or not. For both question, the CMF can be an answer. If you wanna a simple dynamic router, you should have a look into the ChainRouter with an custom router definition:
https://symfony.com/doc/current/cmf/bundles/routing/dynamic.html
and
https://symfony.com/doc/current/cmf/components/routing/chain.html
If you wanna migrate the data, you can use fixture loaders, as we use in almost all of our examples.
With the find and update methods i am able to restrict fetching of data restricted to the logged in user by adding a policy that sets req.options.where.owner = userID, therefore i dont need to create custom controllers or models for these methods.
With update and create i can also set req.options.values.owner = userID so that the user cant create or update an object that will belong to another user.
But the problem is that the blueprint findOne controller does not have any options for this kind of filtering, so any logged in user can request an object created and owned by another user.
Is there anyway i can restrict findOne without writing my own controller and query?
Found a solution to the problem, what you can do is to override the default blueprint action by creating a folder named blueprints in your api folder, there you can create a findone.js (lowercase) file,
copy the original blueprint action from /node_modules/sails/lib/hooks/blueprints/actions/findOne.js to /api/blueprints/findone.js
add .where( actionUtil.parseCriteria(req) ); to the query.
Dont forget to change the path of actionutil from require('../actionUtil'); to require('../../node_modules/sails/lib/hooks/blueprints/actionUtil');
Voila, now the findOne action will respect your req.options.where queries.
You can specify blueprint in your policies like this
module.exports = function (req, res, next) {
var blueprint = req.options.action;
if (blueprint === 'findOne') {
// do restriction here
return next();
}
res.forbidden('not allowed to do something');
};
I'm rather forget, is blueprint name findOne or findone.
I have a few questions that I couldn't find answers anywhere online.
Does sails.js framework support HTTP PATCH method? If not - does anyone know if there is a planned feature in the future?
By default if I create method in a controller it is accessible with GET request is it the routes.js file where I need to specify that method is accessible only via POST or other type of methods?
How would you create a policy that would allow to change protected fields on entity only for specific rights having users. I.e: user that created entity can change "name", "description" fields but would not be able to change "comments" array unless user is ADMIN?
How would you add a custom header to "find" method which specifies how many items there are in database? I.e.: I have /api/posts/ and I do query for finding specific items {skip: 20; limit: 20} I would like to get response with those items and total count of items that would match query without SKIP and LIMIT modifiers. One thing that comes to my mind is that a policy that adds that that custom header would be a good choice but maybe there is a better one.
Is there any way to write a middle-ware that would be executed just before sending response to the client. I.e.: I just want to filter output JSON not to containt some values or add my own without touching the controller method.
Thank you in advance
I can help with 2 and 5. In my own experience, here is what I have done:
2) I usually just check req.method in the controller. If it's not a method I want to support, I respond with a 404 page. For example:
module.exports = {
myAction: function(req, res){
if (req.method != 'POST')
return res.notFound();
// Desired controller action logic here
}
}
5) I create services in api/services when I want to do this. You define functions in a service that accept callbacks as arguments so that you can then send your response from the controller after the service function finishes executing. You can access any service by the name of the file. For example, if I had MyService.js in api/services, and I needed it to work with the request body, I would add a function to it like this:
exports.myServiceFunction = function(requestBody, callback){
// Work with the request body and data access here to create
// data to give back to the controller
callback(data);
};
Then, I can use this service from the controller like so:
module.exports = {
myAction: function(req, res){
MyService.myServiceFunction(req.body, function(data){
res.json(data);
});
}
}
In your case, the data that the service sends back to the controller through the callback would be the filtered JSON.
I'm sorry I can't answer your other questions, but I hope this helps a bit. I'm still new to Sails.js and am constantly learning new things, so others might have better suggestions. Still, I hope I have answered two of your questions.
ISSUE
We just switched from MVC4 Web API Beta to the RC and we're running into a Multiple actions were found that match the request ... exception in our service.
BACKGROUND
We have two POST actions defined in our ApiController:
public class MyModelController : ApiController
{
...
// POST /mymodel
public MyModel Post(MyModel model)
{
...
}
// POST /mymodel/upload
[ActionName("Upload")]
public HttpResponseMessage UploadModelImage()
{
HttpRequestMessage request = Request;
if (!request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.UnsupportedMediaType, request));
}
...
}
}
The first action (default POST action) is used to create a new MyModel object from the JSON passed to the service. The user of our portal has the option to upload an image as part of creating a new MyModel object in which case we use the second Upload action to save the file and persist the new object to the database. This action reads the multipart request content, parses out the properties for the model and saves the image uploaded to our CDN.
Since our switch to the RC, the upload action (http://www.myapidomain.com/mymodel/upload) goes through fine, but the regular POST action (http://www.myapidomain.com/mymodel/) fails with the Multiple actions were found that match the request ... exception citing both the methods listed above as the conflicts.
Here are our routes:
routes.MapHttpRoute(
"Default", // route name
"{controller}" // route template
);
routes.MapHttpRoute(
"OnlyId", // route name
"{controller}/{id}", // route template
new {}, // defaults
new {controller = #"[^0-9]+", id = #"[0-9]+"} // constraints
);
routes.MapHttpRoute(
"OnlyAction", // route name
"{controller}/{action}", // route template
new {}, // defaults
new {controller = #"[^0-9]+", action = ActionNameConstraint.Instance} // constraints
);
routes.MapHttpRoute(
"DependantAction", // route name
"{controller}/{principalId}/{action}/{dependentId}", // route template
new {dependentId = System.Web.Http.RouteParameter.Optional}, // defaults
new {controller = #"[^0-9]+", action = ActionNameConstraint.Instance} // constraints
);
ActionNameConstraint is just a custom constraint that ensures that the {action} must belong to the {controller}
QUESTION
I've tried messing with the routes in different orders to see if that would fix the issue with no luck. I'm looking for help with any of the following solutions:
A potential issue in our routes.
An alternative solution for routing by content-type. The Upload action only needs to be called for mult-part form posts. If the content type is JSON or XML, the regular action should be used. I haven't been able to find any resources that suggest this can be done, but I'm hoping someone else has considered this.
A model-binding approach for reading file streams from the request content so we don't need the separate Upload action anymore
By default it is not possible to mix REST style routing and RPC style routing in a single controller - which it seems you are trying to do.
There is an open issue for that on ASP.NET Web Stack's codeplex, where Web API source lives - http://aspnetwebstack.codeplex.com/workitem/184.
If you want to use it like that, you need to move the upload action to a separate controller, which will be called in an RPC-only way.