I want to make the logic used to create the new GET List endpoint (#1544) available to be used in custom routes, similar to the way getUpdateHandler works.
For security, we'd need the ability to white or blacklist fields, and the fields should be used not just for generating JSON but also limit search / filtering functionality as well (so that nobody can query your data on fields they shouldn't be able to).
I haven't finalised the API for it yet, suggestions are welcome. I'll write up the final API here when I'm about to start coding it. If anyone else wants to take this on, please let me know.
It would look something like this:
List.getAPI(type, options)
Type is for use later, so we can add more endpoint types:
list returns itemsget returns a single item by IDupdate updates a single item by IDdelete deletes a single item by IDExample:
List.getAPI('list', {
fields: 'name, email', // only name and email fields are available
excludeFields: 'password', // alternatively, include all fields except password
filters: 'name, email', // available fields to filter, defaults to the fields option
excludeFilters: 'password', // as above, allow filtering on all fields except password
filterValues: { isAdmin: false }, // always includes these filters
defaultLimit: 500, // alternatively, overrides default limit of 100
limit: 50, // sets limit to this value (otherwise can be specified in query)
defaultSort: 'name', // overrides List defaultSort
sort: 'email' // alternatively, hard-code the sort path
});
Returns a function that can be bound to an Express route:
function (req, res, next) {
// similar to admin/api/list/get.js
}
How would the API handle populating referenced objects? Or should it?
@webteckie I think it would accept a populate option, similar to how the new GET endpoint for the Admin UI has been set up. We need to work out how much granular control to give over this though (e.g. which fields in populated items to return)
On another note GraphQL solves this problem very nicely and I'm really interested in looking at how that could be implemented over KeystoneJS.
makeAPI(), otherwise it's easy to confuse get and GET.@wmertens I'm thinking it may be possible to allow limit as a parameter, probably within bounds, and with a default.
+1 for makeAPI or similar, maybe createAPI. you're right that getAPI is confusing.
For get (retrieve, not list) I think it would default to req.params.id but that should be configurable.
@creynders and I are discussing the API in our Slack at the moment, have you got an invite? if not, shoot me your email address and I'll add you. He's going to update this with some notes in a bit.
@JedWatson I'm at wout.[email protected], thanks!
So, my first API proposal is something like this:
var User = keystone.list('User');
var config = User.expose({ type: true })
.as('people')
.all( 'name -password')
.pre( middleware.requireUser )
.retrieve()
.populate('organization', 'name')
.populate('groups', 'title')
.list('createdAt')
.filter({ age: { $gt: 18}})
.sort('name')
.remove()
.show('-id')
.pre(middleware.requireSelf)
.update('name password')
.pre(middleware.requireSelf)
.create(true)
.populate('organization', 'name')
.pre(middleware.requireAdmin)
.post(middleware.sendUserMail)
app.use(config.routers)
Level 1:
as: allows aliasing the list to another name in the API url, by default the plural name of the list is used, but with as you can something else. E.g. the api for users is exposed at /api/people, not the default /api/usersall: configures routing for all following methods as defaults. Overwritable by specific method configuration.list: configures routing for GET /api/peoplecreate: configures routing for POST /api/peopleretrieve: configures routing for GET /api/people/:id (I prefer retrieve instead of get since get is already used to retrieve settings, might become a bit confusing)update: configure routing for PATCH /api/people/:idremove: configure routing for DELETE /api/people/:idThe above methods (except as) accept field names that will be included (or excluded if prefixed with "-" (minus)) either as output or input of the method.
For all reading methods, i.e. list and retrieve they define which fields will be sent back (merged with the all configuration) I.e. in the above example list will return objects with an id (always automatically included), name (as configured in all) and createdAt (as configured in list) fields.
For all mutating methods the parameters define which fields will be modifiable, i.e. in the above example update allows modifying name and password (but not organization for instance) and the create method allows modification of all fields (a boolean instead of a string will include or exclude all fields)
Level 2:
pre and post allow setting up middleware that needs to run before or after the method resolution itself. E.g. the update method will call a middleware.requireSelf function before its handled. (In the example that would be middleware checking whether the id of the user the mutation is requested for is equal to the id of the logged in user, i.e. you can only modify your own data)populate: all methods return either a collection of resources or the manipulated resource itself (except remove which returns a 204 no content by default) With populate you can populate relationships. These fields will automatically be included to the output, i.e. it's not necessary to write retrieve('organization').populate('organization') just retrieve().populate('organization') is enoughfilter (list-only) filters the collection as specifiedshow: as described above the mutating methods accept field names to include/exclude from modification, but sometimes you want more granular control of what the output of a mutation operation is (by default the resource as configured through retrieve). In the example we want to explicitly drop the id field from the results of the remove operation since the resource won't be available anymore. By calling show we configure the remove operation to return the resource (as configured through retrieve), but we override the automatic inclusion of id. (BTW had we not called show it would've returned a 204 No Content since that's the standard for REST API resource deletions)show is available to the reading methods as well, basically list('createdAt') is syntactic sugar for list().show('createdAt')The object returned from ALL methods is a configuration object with a routers property, which are express routers set up by the build flow. All you need to do is use them.
So, after some input from other people I'm now leaning towards this API:
Level 0
expose: starts of route configuration, for a specific collection. I like it better than createAPI, since it communicates better what you're doing. Maybe it should be exposeAPI?Level 1
see above
Level 2
pre and post as abovequery, accepts a function with mquery, req, res signature. js
Article.expose()
.retrieve()
.query(function(mquery, req, res){
mquery.populate('author', name);
return mquery;
});
With mquery being the mongoose query relevant to the method (in this case retrieve, e.g. Article.findById(articleId)) This allows you to leverage mongoose's powerful and versatile query language.
Some food for thought:
In all you can declare stuff that needs to be applied to all methods (except when overridden) exposing the mongoose query will make this pretty hard, since the mongoose query system has an incremental API, with no means to decrement. E.g. if you declare a populate: 'author' in all you can't unpopulate this in a specific method. I think?
sort, filter and populate become wrappers to the mongoose query methods. show the more I think of it. Need to come up with something better.In general:
js
Article.expose()
.retrieve(function(user){
var fields = 'name author';
user.isAdmin && fields += ' email';
return fields;
});
js
Article.expose({
retrieve: {
show: 'name',
populate: ['author', 'name']
},
list: {
sort: '-createdAt',
query: function(mquery, req, res){
if(!req.user.isAdmin){
mquery = mquery.where({state: 'published'});
}
return mquery;
}
}
});
js
Article.expose()
.retrieve('name')
.populate('createdBy')
.then(function(user, req, res){
user.author = user.createdBy;
delete user.createdBy;
return user;
})
.catch(function(err, req, res){
// a bit convoluted, I know
if(res.locals.status===403){
res.locals.status=404;
res.locals.body='Not found';
}
return err;
});
For Level 0, exposeAPI() would be more clear.
On Fri, Oct 9, 2015 at 12:23 AM, creynders [email protected] wrote:
So, after some input from other people I'm now leaning towards this API:
Level 0
- expose: starts of route configuration, for a specific collection. I
like it better than createAPI, since it communicates better what
you're doing. Maybe it should be exposeAPI?Level 1
see above
Level 2
- ## pre and post as above
new: query, accepts a function with mquery, req, res signature.
Article.expose()
.retrieve()
.query(function(mquery, req, res){
mquery.populate('author', name);
return mquery;
});With mquery being the mongoose query relevant to the method (in this
case retrieve, e.g. Article.findById(articleId)) This allows you to
leverage mongoose's powerful and versatile query language.Some food for thought:
In all you can declare stuff that needs to be applied to all methods
(except when overridden) exposing the mongoose query will make this pretty
hard, since the mongoose query system has an incremental API, with no means
to decrement. E.g. if you declare a populate: 'author' in all you
can't unpopulate this in a specific method. I think?
- sort, filter and populate become wrappers to the mongoose query
methods.- Not too fond about show the more I think of it. Need to come up with
something better.In general:
-
all methods accept functions as input, which will be called at the
appropriate time to configure the query, e.g.Article.expose()
.retrieve(function(user){
var fields = 'name author';
user.isAdmin && fields += ' email';
return fields;
});-
If you prefer to use a configuration object instead of the fluent API,
that's possible too:Article.expose({
retrieve: {
show: 'name',
populate: ['author', 'name']
},
list: {
sort: '-createdAt',
query: function(mquery, req, res){
if(!req.user.isAdmin){
mquery = mquery.where({state: 'published'});
}
return mquery;
}
}
});-
Some more food for thought: error handling and data massaging. Maybe
it's a good idea to allow the use of a promise-like API to handle both e.g.:Article.expose()
.retrieve('name')
.populate('createdBy')
.then(function(user, req, res){
user.author = user.createdBy;
delete user.createdBy;
return user;
})
.catch(function(err, req, res){
// a bit convoluted, I know
if(res.locals.status===403){
res.locals.status=404;
res.locals.body='Not found';
}
return err;
});—
Reply to this email directly or view it on GitHub
https://github.com/keystonejs/keystone/issues/1585#issuecomment-146777390
.
Would this feature include the ability to specify search terms in a List API request?
(edit) if so, how far away is this from being available? If not, is there another recommended way for allowing an external system to search within Keystone's data?
@JedWatson @mxstbr What is the status on this?
Again any update on status?
Keystone 4 is going in maintenance mode. Expect no major change. see #4913 for details.
Keystone v5 is based on GraphQL and is more flexible.