/ *
This is the analog to ` lib/api.js ` and provides features that help work
consistently with the JSON data returned by ` commands/api.js ` handlers in
` api/ ` . This library is designed so you can start working on your UI without
anything in the backend . That means you can create "mock" routes using the
` mock() ` function , design your data along with your UI in the same file , and
then "move" that configuration to an ` api/ ` handler right after .
# # # Getting Started
Easiest way to get a client going is to use the ` bando.js djent ` command :
` ` ` shell
node bando . js djent -- template . / static / djenterator / client . svelte -- output client / pages / Test . svelte
` ` `
You can also go to the [ Djenterator ] ( http : //127.0.0.1:5001/admin/#/djenterator/) with your
browser to visually alter the output . I recommend this if you already know what you want
your data mock to start with .
# # # Updating Routes
Once you run this example command you ' ll have a file in
` client/pages/Test.svelte ` . Currently you manually add these the file
` client/routes.js ` like this :
` ` ` javascript
import Test from "./pages/Test.svelte" ;
export default {
"/test/" : Test ,
}
` ` `
If you open this file now you ' ll see many other routes that make up the stock
web application . Obviously you ' ll "augment" this file , rather than use the
above as the only contents .
# # # UI First
After this you need to edit the ` client/pages/Test.svelte ` file , and use the
sample data in the mock configuration :
` ` ` javascript
api . mock ( {
"/api/user/profile" : {
"get" : [ 200 , { "message" : "OK" } ] ,
}
} ) ;
` ` `
You can then use the ` client/api.js ` functions to work with this pretend data
to develop your UI . Just do your ` api.get ` ( or ` post ` , or whatever ) and
augment the fake data you return as you create the visual experience .
# # # Craft Tests ?
Testing can come right at this point if you use the Playwright testing setup
in ` lib/testing.js ` . Since you have the UI sort of working , you can take a
bit of time writing a test and speed up the later steps . The idea is you
write your test to click on all the buttons , fill out forms , and cause
errors . When the tests mostly run with your fake UI then later development
steps will be quicker because you ' ll automate testing that the new code
works .
For example , if you have a working test for a fake UI , then you craft a
` api/ ` handler , your test will tell you that your new ` api/ ` is properly
replacing your fake data automatically .
# # # Migrate to ` api/ `
When your UI is pretty good you can take the fake data you ' ve been working on in ` api.mock() `
and simply move it to your ` api/ ` handler . Just generate one using the
[ Djenterator ] ( http : //127.0.0.1:5001/admin/#/djenterator/) but plug in your fake data.
This will craft a handler that returns this data , and in _theory _ this will then keep working
with your UI .
Once you have your fake data flowing out of the new ` api/ ` handler then you
can work on the form validation , logic , and other things you need to refine
the UI further .
# # # Create Models
The final step is to take your futher developed data and create a model for it in ` lib/models.js ` .
This will usually require crafting a migration with :
` ` ` shell
npm run knex migrate : migrate some _unique _description
` ` ` `
# # # Refine Tests
If you made a UI test then in _theory _ most of your testing is done and just needs refinement . You
should try to cause as many errors as you can , and then use the coverage system to make sure you ' re
at _least _ running all of the code . It ' s hard to hit every line , but aim for as high as you can .
After that , I recommend tests for your Models , but tests that hit the ` api/ `
are usually low value as long as you ' re working the UI with Playwright . The
reason is the _UI _ is already hitting the ` api/ ` so other tests are largely
duplicate .
* /
const MOCK _ROUTES = { } ;
import { user , cache _reset } from "./stores.js" ;
import { log } from "$/client/logging.js" ;
import Validator from 'Validator' ;
/ *
This performs a _client side _ validation of a form , updating the internal metadata
so your ` FormField.svelte ` displays errors . You can look in ` client/components/Login.svelte `
for an example , but the data format is :
` ` ` javascript
let form = {
email : "" ,
password : "" ,
_valid : false ,
_errors : { } ,
_rules : {
email : 'required|email' ,
password : 'required'
}
}
` ` `
One feature of ` lib/api.js:API.validate() ` is it returns the validation rules you create in the ` api/ ` handler . This means you can keep validation rules in the most important place : the backend . You then submit your first form attempt , handle the error , and update this form with the form the backend returns .
If you do this , then your UI pattern is :
1. No validation feedback on the first form fillout . This improve usability as it doesn 't confuse the user with fake errors while they' re typing .
2. Submit the form , then the ` api/ ` handler validates , and returns the validation Rules in the response .
3. After this response your UI handles the validation with local fast UI feedback , and when it ' s correct submits it again to the ` api/ ` handler .
4. Your ` api/ ` handler then still keeps validating , but there ' s less useless network traffic and faster user feedback .
I 've found this pattern is better for security and usability. It' s more usable because the user
isn ' t slammed with useless validations while they type , giving them a chance to make edits and fix
problems . It ' s more secure because the rules for validation and the true validation are all in the
` api/ ` handler backend where it 's required. The Validation rules can also come from the `lib/models.js:Model.validation` generator so they' re based on the database schema .
+ ` form Object ` -- The form fields , with any validation settings . Things starting with ` _ ` are considered internal .
+ ` extra(form) ` -- A callback you can use to do additional validation too complex for the rules .
* /
export const validate = ( form , extra ) => {
if ( form . _rules ) {
let validation = Validator . make ( form , form . _rules ) ;
form . _valid = validation . passes ( ) ;
if ( extra ) extra ( form ) ;
form . _errors = validation . getErrors ( ) ;
return form ;
} else {
return form ;
}
}
/ *
Determines if the form can be submitted . If you read the docs for ` validate ` I say you
should submit the first attempt , then validate the remaining ones in the browser ( and backend ) .
This function helps you do that as it will say ` true ` if there are no ` form._rules ` set .
These only get set when you handle the response from the ` api/ ` handler , so the first request
will go through , then after that this function looks for ` form._valid ` .
+ ` form Object ` -- The form fields , with any validation settings .
* /
export const can _submit = ( form ) => {
return form . _rules === undefined || form . _valid ;
}
/ *
Cleans a form of junk before submitting to the ` api/ ` handler . It removes
+ ` form Object ` -- The form fields , with any validation settings .
+ ` extras Array ` -- Additional fields to remove .
* /
export const clean _form = ( form , extras = [ ] ) => {
form . _errors = { } ; // errors is accessed so needs to exist
delete form . _valid ;
delete form . _rules ;
for ( let field of extras ) delete form [ field ] ;
}
/ *
The default fetch _opts are ` { credentials: 'same-origin',} ` to enforce
authentication . Don ' t really change these here unless you know what
you ' re doing .
* /
export const fetch _opts = { credentials : 'same-origin' } ;
/ *
Create a mock data response , so you can pretend you have a ` api/ ` handler
without having to actaully make one at first . This makes development easier
since you don ' t need to bounce around between tons of files just to get a
UI going .
# # # Config Format
The primary key is the URL , and it contains an object mapping each ` get ` , ` post ` , etc .
to the data returned . Here ' s an example :
` ` ` javascript
api . mock ( {
"/api/user/profile" : {
"get" : [ 200 , { "message" : "OK" } ] ,
}
} ) ;
` ` `
You can have multiple URLs and multiple actions per URL , but the data returned is static .
If you 're at a point where you need to change the data response at random then it' s time
to write an ` api/ ` handler .
+ ` config Object ` -- Configuration of routes to data response .
* /
export const mock = ( config ) => {
for ( let [ route , value ] of Object . entries ( config ) ) {
MOCK _ROUTES [ route ] = value ;
}
}
/ *
The internal version of ` raw() ` that does a mock call instead .
The ` raw() ` functionw will call this if the URL is in ` api.mock() ` .
Otherwise it ' s mostly internal .
* /
export const raw _mock = ( url , raw _method , body , unauthed _action ) => {
let error ;
const method = raw _method . toLowerCase ( ) ;
const config = MOCK _ROUTES [ url ] ;
const data = config [ method ] ;
if ( data === undefined ) {
error = ` Mock ${ url } : ${ method } is not in your mocks list. Did you forget to add it to the api.mock call? ` ;
} else if ( data . length !== 2 ) {
error = ` Mock ${ url } : ${ method } does not have the correct config. Must be [status (int), {data}], like [200, {message: 'OK'}]. `
}
if ( error ) {
log . error ( error , MOCK _ROUTES ) ;
return [ 500 , { message : error } ] ;
} else {
// this is the same as res.status below
if ( unauthed _action && ( data [ 0 ] === 401 || data [ 0 ] === 403 ) ) unauthed _action ( ) ;
return data ;
}
}
/ *
JavaScript ' s URL parsing only things full complete URLs with ports and hosts are
real URLs . This fakes it out .
* /
const parse _url _because _js _is _stupid = ( url ) => {
try {
// javascript is dumb as hell and thinks a typical /this/that is not a URL
return new URL ( url ) ;
} catch ( error ) {
// so fake it for the mock by tacking on ... localhost then ignoring it
return new URL ( ` http://localhost ${ url } ` ) ;
}
}
/ *
All of the calls to ` api/ ` handlers are mostly the same , so this one
function does them all . This is then called by every request method
like ` get() ` and ` post() ` . It ' s used internally so only access it if
you ' re really desperate .
The main thing to understand is that ` client/api.js ` tries to normalize
as much as possible , including authentication . When the handler returns
a 403 / 401 response this function will return the error like normal , but
if you add the ` unauthed_action() ` callback then you can catch this and
do a redirect to the login page . Look at the ` client/components/LoggedIn.svelte `
for an example of using this .
_ _ _FOOTGUN _ _ _ : This calls ` raw_mock ` if the URL you requested is in a ` api.mock() `
specification . If you ' re having trouble with requests not going through delete
your ` api.mock() ` .
+ ` url string ` -- URL to request .
+ ` method string ` -- ALL CAPS METHOD NAME LIKE POST GET .
+ ` body Object ` -- Body for the JSON request .
+ ` unauthed_action() ` -- Callback for what happens when a 403 / 401 is returned .
* /
export const raw = async ( url , method , body , unauthed _action ) => {
const parsed = parse _url _because _js _is _stupid ( url ) ;
// if there is a rules and valid is exactly false we should just run
// the validation here and avoid the request
if ( body && body . _rules && body . _valid === false ) {
const res = validate ( body ) ;
if ( ! can _submit ( res ) ) {
log . debug ( "Form invalid, won't HTTP submit." ) ;
return [ 400 , res ] ;
} else {
clean _form ( body ) ;
}
}
if ( parsed . pathname in MOCK _ROUTES ) {
return raw _mock ( parsed . pathname , method , body ) ;
} else {
let options = {
method ,
credentials : 'same-origin' ,
headers : {
'Content-Type' : 'application/json' ,
} ,
}
if ( body ) options . body = JSON . stringify ( body ) ;
/ * t h i s h a n d y l i t t l e g e m e x i s t s b e c a u s e f e t c h l i k e s t o g o :
* " the string did not match the expected pattern ”
* when the response body is not json ... or at least that ' s
* what I think it means . Now I get the text , and use it
* for logging .
* /
let res = await fetch ( url , options ) ;
let text = await res . text ( ) ;
try {
if ( unauthed _action && ( res . status === 401 || res . status === 403 ) ) unauthed _action ( ) ;
return [ res . status , JSON . parse ( text ) ] ;
} catch ( error ) {
log . error ( error , "Failed to parse reply body as JSON. Text is:" , text , "error" , error , "URL" , url ) ;
return [ 500 , { "message" : "Exception processing request. See log.debug." } ] ;
}
}
}
/ *
Used for fetching basic resources , not JSON data from the API . Uses the
same return signature as ` raw() ` but doesn ' t attempt a JSON conversion ,
and only does a "GET" request . It returns the result of ` res.blob() `
so you can convert it to a string or JSON as you need , or leave it if
it ' s a binary file .
+ ` url string ` -- URL to get .
+ _ _ _return _ _ _ ` Array[status, Blob|{}] ` -- Returns a Blob or when status is 500 a ` {"message": "Error message"} ` .
* /
export const blob = async ( url ) => {
let data ;
try {
let res = await fetch ( url , fetch _opts ) ;
data = await res . blob ( ) ;
return [ res . status , data ] ;
} catch ( error ) {
log . error ( error , "Failed to parse reply body as JSON. Text is:" , data , "error" , error , "URL" , url ) ;
return [ 500 , { "message" : "Exception processing request. See log.debug." } ] ;
}
}
/ *
The GET method request . To keep things consistent with the
other requests this accepts a ` data ` parameter , but it URL
encodes them and attaches them to the URL before calling ` raw() ` .
+ ` url string ` -- The url to request .
+ ` data Object ` -- Data to URL encode and append to the URL .
+ ` unauthed_action() ` -- Callback on authentication failure to let you redirect to a login .
* /
export const get = async ( url , data , unauthed _action ) => {
const params = new URLSearchParams ( data || { } ) ;
const param _url = ` ${ url } ? ${ params . toString ( ) } ` ;
return await raw ( param _url , 'GET' , undefined , unauthed _action ) ;
}
/ *
The POST method request . Refer to ` raw() ` for more documentation .
+ ` url string ` -- The url to request .
+ ` data Object ` -- Data to URL encode and append to the URL .
+ ` unauthed_action() ` -- Callback on authentication failure to let you redirect to a login .
* /
export const post = async ( url , data , unauthed _action ) => {
return await raw ( url , 'POST' , data , unauthed _action ) ;
}
/ *
The PUT method request . Refer to ` raw() ` for more documentation .
+ ` url string ` -- The url to request .
+ ` data Object ` -- Data to URL encode and append to the URL .
+ ` unauthed_action() ` -- Callback on authentication failure to let you redirect to a login .
* /
export const put = async ( url , data , unauthed _action ) => {
return await raw ( url , 'PUT' , data , unauthed _action ) ;
}
/ *
The DELETE method request . Refer to ` raw() ` for more documentation .
+ ` url string ` -- The url to request .
+ ` unauthed_action() ` -- Callback on authentication failure to let you redirect to a login .
* /
export const del = async ( url , unauthed _action ) => {
return await raw ( url , 'DELETE' , undefined , unauthed _action ) ;
}
/ *
The OPTIONS method request . Refer to ` raw() ` for more documentation .
+ ` url string ` -- The url to request .
+ ` unauthed_action() ` -- Callback on authentication failure to let you redirect to a login .
* /
export const options = async ( url , unauthed _action ) => {
return await raw ( url , 'OPTIONS' , undefined , unauthed _action ) ;
}
/ *
Properly logs the user out of the backend _and _ frontend . The one
major drawback to the SPA model is you have to sync the user ' s state
with the backend . This cleans out the ` client/stores.js:user ` state ,
resets any caches being used , send a ` get('/api/logout') ` , and redirects
the window to ` /client/#/login ` .
* /
export const logout _user = async ( ) => {
user . update ( ( ) => {
return { authenticated : undefined }
} ) ;
// really only a problem for multiple people
// using the same browser, but clear it out
cache _reset ( ) ;
let [ status , data ] = await get ( '/api/logout' ) ;
if ( status !== 200 ) log . error ( "Invalid status from logout" , status , data ) ;
window . location . replace ( "/client/#/login" ) ;
}
/ *
Gets the schema for the database , which isn ' t needed for most operations ,
but if you ' re working on anything that modifies the database or needs the
database schema this is useful .
_ _ _WARNING _ _ _ : This is only accessible to users with ` admin=1 ` and blocked in the
` api/admin/schema.js ` handler . Don ' t think that because this is in ` client/api.js `
that the schema information should be exposed to the internet . The handler is read
only , but you never know what weird things people can figure out from your schema .
* /
export const schema = async ( table ) => {
let [ status , tables ] = await get ( '/api/admin/schema' ) ;
if ( status == 200 ) {
for ( let t of tables ) {
if ( t . name === table ) {
// this exits the function with the schema
return t . _columns ;
}
}
// this happens if the table isn't in the schema
return undefined ;
} else {
return undefined ;
}
}
export default {
post , get , put , del , mock , options , fetch _opts ,
logout _user , validate , can _submit , clean _form , schema , blob
}