README
+TITLE: @apparts/model-api
+DATE: [2021-02-08 Mon]
+AUTHOR: Philipp Uhl
Generate CRUDish API-endpoints for a model defined with the [[https://github.com/phuhl/apparts-model][@apparts/model]] package.
- Installation
+BEGIN_SRC js
npm i --save @apparts/model-api
+END_SRC
- Usage
To generate CRUD-like API-endpoints for a model, use the =addCrud= function.
- =addCrud({ prefix, app, model, routes, webtokenkey})=
- =prefix : string= :: The prefix for the URI, can contain parameters (see next section).
- =app : App= :: The express app
- =model : function= :: A function that returns an array in the form of =[OneModel, ManyModel, NoneModel]= (as does the =makeModel= function from [[https://github.com/phuhl/apparts-model][@apparts/model]] package).
- =routes : object= :: An object that contains options for the routes
to be generated. If one of the keys is not present in the object,
that route will not be generated. Possible keys are:
- =get= :: The get all route
- =getByIds= :: The get by ids route
- =post= :: The create item route
- =put= :: The update item route
- =delete= :: The delete items route The values for each of these keys are objects, that can contain:
- =access : (request, me) => boolean= :: A functions that defines access rights for the individual endpoint. It receives the same parameters as a request handler from [[https://github.com/phuhl/apparts-types][@apparts/types]] receives. It returns a boolean, that decides, if access should be granted or not.
- =title : string= :: A custom title for the documentation
- =description : string= :: A custom description for the documentation
- =webtokenkey : string= :: The key to be used for the JWT verification (done by =prepauth.prepauthTokenJWT= from [[https://github.com/phuhl/apparts-types][@apparts/types]])
+BEGIN_SRC js
const { addCrud, accessLogic: { anybody } } = require("@apparts/model");
const addRoutes = (app) => { // ...add your routes
addCrud("/v/1/user", app, useUser, { get: anybody, getByIds: anybody, post: anybody, put: anybody, delete: anybody, }); };
+END_SRC
Adds these routes:
- =GET /v1/user= :: The get all route (=get=)
- =GET /v/1/user/:ids= :: The get by ids route (=getByIds=)
- =POST /v/1/user= :: The create item route (=post=)
- =PUT /v/1/user/:id= :: The update item route (=put=)
- =DELETE /v/1/user/:ids= :: The delete items route (=delete=)
All these routes are secured by =prepauth.prepauthTokenJWT= from the [[https://github.com/phuhl/apparts-types][@apparts/types]] package. That means, that requests have to contain a JWT that is accepted by =prepauth.prepauthTokenJWT=. If you want to allow access without an account, you can generate a JWT that serves as an API-Key and provide it as the authentication on not-logged in users.
** Custom route prefixes for more REST-style access
+BEGIN_SRC js
const { addCrud, accessLogic: { anybody } } = require("@apparts/model");
// create comment model with this type:
const types = {
id: {
type: "id",
public: true,
auto: true,
key: true
},
userid: { type: "id", public: true },
createdOn: {
type: "time",
default: (c) => c.optionalVal || Date.now()
},
comment: { type: "string", public: true },
};
// add routes addCrud("/v/1/user/:userid/comment", app, useComments, { get: anybody, getByIds: anybody, post: anybody, put: anybody, delete: anybody, });
+END_SRC
Adds these routes:
- =GET /v/1/user/:userid/comment=
- =GET /v/1/user/:userid/comment/:ids=
- =POST /v/1/user/:userid/comment=
- =PUT /v/1/user/:userid/comment/:id=
- =DELETE /v/1/user/:userid/comment/:ids=
Note, that the parameter =userid= from the route is /automatically/ /matched/ against the =userid= field from the model.
** Custom access management
In the previous examples, all routes where created accessible for anybody (with a valid JWT). That is most likely not what you want. Instead, you can define a function for each crud operation that returns a boolean. This function receives all parameters of the API-call and uses them to determine if access should be granted. Only if it returns =true=, access will be granted. The function can be =async=, too.
+BEGIN_SRC js
addCrud("/v/1/user/{userid}/comment", app, useComments, { get: async ({ dbs, params: { userid } }, me) => { // I can only list comments from my friends const [,User] = useUser(dbs); const meUser = await new User().loadById(me.userid); return meUser.content.friends.indexOf(userId) !== -1; }, // I can read every commend I have the id for getByIds: () => true, // I can only post comments in my name post: ({ params: { userid } }, me) => userid === me.userid, // I can only edit my own comments put: ({ params: { userid } }, me) => userid === me.userid, // I can only delete my own comments delete: ({ params: { userid } }, me) => userid === me.userid, });
+END_SRC
For convenience some helpers are defined that support combining multiple access decider functions:
+BEGIN_SRC js
const { addCrud, accessLogic: { or, orS, anybody } } = require("@apparts/model");
const isAdmin = (_, { role }) => role === "admin"; const isUser = ({ params: { userid } }, me) => userid === me.userid; const canListUsers = (ps) => { // ... };
addCrud("/v/1/user/{userid}/", app, useComments, { // here, use "orS" to reduce database load (as orS is lazy) or "or" to optimize for return time get: orS(isAdmin, canListUsers), getByIds: anybody, post: isUser, put: or(isAdmin, isUser), delete: or(isAdmin, isUser), });
+END_SRC
The helper functions are:
+BEGIN_SRC js
// check all conditions in parallel const and = (...fs) => async (...params) => await Promise.all(fs.map(f => f(params...))); const or = (...fs) => async (...params) => await Promise.race(fs.map(f => f(params...)));
// check all conditions in sequence const andS = (...fs) => async (...params) => await fs.reduce(async (a, b) => await a && await b(), Promise.resolve(true)); const orS = (...fs) => async (...params) => await fs.reduce(async (a, b) => await a || await b(), Promise.resolve(false));
// anybody const anybody = () => true;
+END_SRC
** Special parameters in the model
When defining the type of your model, you can use all the parameters as defined by [[https://github.com/phuhl/apparts-model][@apparts/model]] (e.g. =public=, =mapped=, =optional=, =derived=, =auto=). The generated API endpoints respect these values:
- Only types with =public: true= are shown on GET and can be set with POST and PUT
- Types with =mapped: true= are shown to the outside with their mapped names
- Types with =optional: true= are optional and don't have to be set
- Types with =auto= or a =derived= function can not be set on PUT or POST
- The =derived= function can be used to fetch sub object as the =derived= function is called asynchronously.
Additionally, @apparts/model-api respects the value =readOnly=:
- Types with =readOnly: true= can only be read. It's value have to be created with a =default= function. This can be useful, e.g. for a created date, that should be readable (i.e. public) but not be modifiable.