/ *
_ _WARNING _ _ : Early documentation . This should be treated as notes to
remind you of how to use the ` api/ ` handlers with this , rather than
a full guide .
This is a convenience library that helps with normalizing JSON API
requests and responses with [ Express . js ] ( https : //expressjs.com). The
` API ` class contains useful functions for redirecting , building replies ,
sending back errors , authenticating users , and validating forms . It is
primarily used in writing ` api/ ` handlers .
# # How ` api/ ` Handlers Run
There ' s a command in ` commands/api.js ` that runs everything , and contains a lot
of tricks for Express . js that you should study . You run this with the ` bando.js `
command runner like this :
` ` ` shell
node bando . js api
` ` `
This command then imports all of the files in ` api/ ` recursively , as well as
the ` socket/ ` handlers , and sets them up to handle requests . Requests are mapped
by URL to ` api/ ` by taking the ` .js ` off the file and using that as the URL . So
` api/running.js ` becomes ` /api/running ` .
# # Writing ` api/ ` Handlers
The code in ` api/ ` is mapped to URLs almost directly , so to create a new
URL just make a file in that directory . For example , if you want ` /api/running `
as a check if your server is running , then create the file ` api/running.js ` .
The easiest way to make this file is to use the ` node bando.js djent ` command like this :
` ` ` shell
node bando . js djent -- template static / djenterator / api . js -- output api / running . js
` ` `
This will use the template in ` static/djenterator/api.js ` to craft a starter file
for your ` api/running.js ` . If you want to change the output you can access
[ http : //127.0.0.1:5001/admin/#/djenterator/](http://127.0.0.1:5001/admin/#/djenterator/)
and visually choose the template to change , and edit the ` .json ` variables file it uses .
You should study this template output for all of the things you might need , although most
of it you ' ll strip out in this little tutorial .
# # Handling Requests
To handle a ` GET ` , ` POST ` , or other request you simply create an exported function that
matches the lowercase version of the name :
` ` ` javascript
export const get = async ( req , res ) => {
// do you thing here
}
` ` `
In the example file ` api/running.js ` you just created with ` bando.js djent `
you should see this function . Next you ' ll strip out everything that has
nothing to do with replying with a status .
# # Using ` class API `
You create the ` API ` class using the Express ` request ` and ` response `
objects in your ` api/ ` handler , then use the methods ` .reply() ` , ` .redirect() ` ,
or ` .error() ` to report status of the HTTP request . Let ' s strip down
your generated ` api/running.js ` file to the minimum necessary :
` ` ` javascript
import logging from '../lib/logging.js' ;
import { API } from '../lib/api.js' ;
const log = logging . create ( import . meta . url ) ;
export const get = async ( req , res ) => {
const api = new API ( req , res ) ;
try {
api . reply ( 200 , { message : "OK" } ) ;
} catch ( error ) {
log . error ( error ) ;
api . error ( 500 , error . message || "Internal Server Error" ) ;
}
}
` ` `
Here you can see I am only leaving logging , ` API ` , and returning simple replies
with ` api.reply(200, {message: "OK")) ` or a more complex error reply if that fails .
# # Authenticating Requests
Authentication for a handler is very simple and mostly done for you :
` ` ` javascript
// add this after the above get method
get . authenticated = true ;
` ` `
You "tag" any handlers you want protected with authentication by setting the ` authenticated `
variable on them . The ` commands/api.js ` runner then knows to demand authentication before
running this handler . When it is run you can then use ` API.user ` to get at the authenticated
user .
# # Form Validation
Validating forms in a server is very important , as you can ' t trust that any
browser actually performed the validation . The ` API ` class uses the wonderful
[ Laravel validation rules ] ( https : //laravel.com/docs/9.x/validation) via the
very nice [ Validation ] ( https : //www.npmjs.com/package/Validator) module by
[ jfstn ] ( https : //github.com/jfstn/Validator). These rules are also the same
as returned by the ` lib/ormish.js:Model.validation ` method . That means you
can :
1. Get your Model from ` lib/models.js ` .
2. Call ` TheModel.validation(rules) ` with empty '' for any rules you want filled in based on the database schema .
3. Pass those rules to ` API.validate() ` to check inputs match the Model ' s rules .
4. Return a ` API.validation_error() ` when they fail .
Here ' s an example taken from the ` api/register.js ` handler , but stipped down to
the essentials so it only validates a user form then returns the cleaned up form :
` ` ` javascript
import { User , Site , } from "../lib/models.js" ;
const rules = User . validation ( {
email : '' ,
full _name : '' ,
initials : 'required|alpha|max:3' ,
password _repeat : 'required|same:password' ,
password : '' ,
tos _agree : 'required|boolean|accepted' ,
} ) ;
export const post = async ( req , res ) => {
const api = new API ( req , res ) ;
const form = api . validate ( rules ) ;
if ( ! form . _valid ) {
return api . validation _error ( res , form ) ;
} else {
api . clean _form ( form , rules ) ;
api . reply ( 200 , { clean _user : form } ) ;
}
}
` ` `
# # A Good Validation Pattern
Validating forms and input is ver important , but you want to do it in a way that
doesn 't interfere with a user' s typing , yet still uses the client - side browser to
speed up the validation feedback . There ' s also the problem that _all _ form validation
systems seem to need the validation rules in both the backend and frontend of the
application .
I ' ve found the best pattern to handle all of these is to " validate on first submit , return
the rules for later submits . " It works like this :
1. On the first view of the form do _not _ provide any validation feedback . This removes the annoying noise of fields claiming inputs are wrong when they 're simply being worked on. For example, as someone types their email it' s annoying to flash it right / wrong until they ' re done .
2. Always validate based on the rules , and even better based on the database schema .
3. When this first validation fails , return a validation failure _and _ return the rules for the browser to use for later attempts .
4. The browser then has the rules , shows that there was an error , and can then provide immediate fast feedback to the user and block submits until its correct .
5. This also places the rules in only one place : the backend , where they ' re absolutely required .
# # Client Side Validation
The validation process I described needs one more component on the client side , which is
demonstrated in the ` client/pages/Registration.svelte ` page :
` ` ` javascript
const register = async ( ) => {
let [ status , data ] = await api . post ( "/api/register" , form ) ;
if ( status === 200 ) {
$user . new _registration = true ;
push ( '/' ) ;
} else {
form = Object . assign ( form , data ) ;
}
}
` ` `
The ` api.post("/api/register", form) ` then uses ` API.validation_error ` to return an
error response which contains the errors _and _ validation rules that the browser
should use . Remember , the ` api.post() ` above is from ` client/api.js:post ` while
the ` API.validation_error ` is from this file .
_ _ _BUG _ _ _ : Currently ` client/pages/Registration.svelte ` doesn ' t demonstrate the other part
of using ` client/api.js:validate ` to do more validations in the browser .
# # Going Further
Once you get this working that ' s mostly all there is to writing an ` api/ ` handler in
The Bandolier . You ' ll then want to learn how ` lib/ormish.js ` and ` lib/models.js ` works ,
and how to create new UIs in ` client/ ` or ` admin/ ` . On the client side you ' ll want to
study the ` client/api.js ` which is the counterpart to this module , and look at the
` client/components/FormField.svelte ` plus ` client/pages/Registration.svelte ` for other
examples .
* /
import Validator from 'Validator' ;
/ *
Used internally to handle security decisions based on whether someone
runs the app with ` DANGER_ADMIN=1 ` environment variable set or not .
Originally I was very loose in how this can be set , with sometimes ` DANGER_ADMING=true `
or ` DANGER_ADMIN="1" ` , but I ' ve since normalized that it will _only _ work if
` DANGER_ADMIN==="1" ` .
* /
export const developer _admin = process . env . DANGER _ADMIN === "1" ;
/ *
The main class you use in ` api/ ` handlers . Primarily used like this :
` ` ` javascript
import { API } from '../lib/api.js' ;
export const get = async ( req , res ) => {
const api = new API ( req , res ) ;
}
` ` `
It is a very thin layer over the regular Express . js request / response objects ,
but it ' s main purpose is to return consistent errors when validation fails .
If you don ' t want to use this , then look at ` validation_error ` to see what
you need to replicate for form validation .
* /
export class API {
/ *
Constructor for the ` API ` class . Takes the Express . js request / response
objects .
+ ` req Object ` -- Express . js Request object .
+ ` res Object ` -- Express . js Response object .
* /
constructor ( req , res ) {
this . req = req ;
this . res = res ;
}
/ *
Simply returns a status and JSON data to the browser . This normalizes
the type of the response to it ' s always JSON . I use this with ` return `
even though it doesn ' t matter , mostly to make sure the control flow
exits when I want . This also helps ` eslint ` detect when I ' ve missed a
branch on the returns . Ultimately Express . js doesn ' t care about the
return so it ' s harmless .
+ ` status number ` -- The status code as an integer .
+ ` data Object ` -- The object to encode as JSON . Must be JSON . stringify ( ) capable .
+ _ _ _return _ _ _ undefined -- Whatever ` res.status().json() ` returns .
* /
reply ( status , data ) {
return this . res . status ( status ) . json ( data ) ;
}
/ *
Redirects the browser with status 301 by default . If you want a different
status then change the second parameter ( which defaults to 301 ) .
+ ` url string ` -- The URL to redirect the browser to .
+ ` status number (301) ` -- The redirect status code to use , by default 301.
* /
redirect ( url , status = 301 ) {
return this . res . redirect ( status , url ) ;
}
/ *
Returns an error status to the browser . The ` err ` parameter can either
be a plain ` string ` or an Object . Since this is an entirely JSON API
system the function does this :
1. If it ' s a string , it wraps it in ` {message: err} ` .
2. If it ' s an object is sends it directly as JSON .
+ ` status number ` -- Status code for the error .
+ ` err (string|Object) ` -- Either a string message that will be wrapped , or a raw object .
* /
error ( status , err ) {
if ( typeof err === 'string' ) {
return this . res . status ( status ) . send ( { message : err } ) ;
} else {
return this . res . status ( status ) . json ( err ) ;
}
}
/ *
A ` get accessor ` that returns whether the current user is authenticated
or not . Normally you would tag your handler as requiring authentication
with ` get.authenticated = true ` but there ' s some instances when you need
to do your own auth inside a handler .
+ _ _ _return _ _ _ boolean -- whether the user is authenticated
* /
get user _authenticated ( ) {
return this . req . user !== undefined ;
}
/ *
Another ` get accessor ` that tells you if this user is authenicated _and _ an
admin . An admin user is defined by having the ` User.admin ` field of the ` user `
table in the database set to 1.
+ _ _ _return _ _ _ boolean -- whether the user is authenticated and an admin .
* /
get admin _authenticated ( ) {
return this . req . user !== undefined && this . req . user . admin === 1 ;
}
/ *
A ` get accessor ` that retuns the current user object if they ' re authenticated .
+ _ _ _return _ _ _ User -- the currently authenticated user .
* /
get user ( ) {
return this . req . user ;
}
/ *
Validates the rules given using the Laravel rules system . You can combine
with with ` Model.validation ` to generate the rules based on the schema . You
can use ` extra ` to pass in a callback to do extra validation . The ` extra(form) `
callback has access to the final result _after _ all validation has ran , so you
can alter the results how you want .
_ _ _WARNING _ _ _ : This documentation needs to be improved to show exactly how to use it .
+ ` rules Object ` -- Uses the validtion rules system from Laravel . See this module ' s docs at the top for more information .
+ ` extra(form) ` -- - Callback that takes the validated form and does additional validation or cleaning .
* /
validate ( rules , extra ) {
const form = this . req . method === "GET" ? this . req . query : this . req . body ;
let validation = Validator . make ( form , rules ) ;
// keep the rules as they're needed everywhere
form . _rules = rules ;
// BUG: validator has a bug that considers an empty
// form valid even if there are required rules
if ( Object . getOwnPropertyNames ( form ) . length === 0 ) {
// add in a fake entry in the form
form [ "_empty" ] = null ;
// validate it
form . _valid = validation . passes ( ) ;
// then remove _empty to keep it clean
delete form [ "_empty" ] ;
} else {
form . _valid = validation . passes ( ) ;
}
form . _errors = validation . getErrors ( ) ;
if ( extra ) extra ( form ) ;
return form ;
}
/ *
Returns a properly formatted validation error to the browser . This can then be used by
the ` client/components/FormField.svelte ` to display validation errors , and since the
rules are included in the response you can do further validation in the browser .
+ ` res Object ` -- The response object from Express . js .
+ ` form Object ` -- The failed form from ` .validate() ` .
* /
validation _error ( res , form ) {
return res . status ( 400 ) . json ( form ) ;
}
/ *
Cleans a form you ' ve received from the internet to have only fields
matching ` rules ` . You can also give an ` extra ` list of fields to
keep . You combine this with ` Model.validation ` to create a way to
remove bad inputs from submitted forms before validation .
+ ` form Object ` -- The form to clean before validation .
+ ` rules Object ` -- The rules to base the cleaning on .
+ ` extra Array ` -- A list of additional fields to keep .
* /
clean _form ( form , rules , extras = [ ] ) {
// what I wouldn't give for set operations
for ( let [ key , rule ] of Object . entries ( form ) ) {
if ( ! ( key in rules ) ) {
delete form [ key ] ;
}
}
for ( let field of extras ) {
delete form [ field ] ;
}
}
}
/ *
BUG : This is a copy of the ` defer() ` in ` client/helpers.js ` but
really both of these should be in another library .
* /
export const defer = ( ) => {
let res ;
let rej ;
let promise = new Promise ( ( resolve , reject ) => {
res = resolve ;
rej = reject ;
} ) ;
promise . resolve = res ;
promise . reject = rej ;
return promise ;
}