@apparts/model-api

Generate CRUDish API-endpoints for a model defined with the @apparts/model package

Usage no npm install needed!

<script type="module">
  import appartsModelApi from 'https://cdn.skypack.dev/@apparts/model-api';
</script>

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.