@appunto/api-on-json v0.3.5
API On JSON
This library was build for and is used by Appunto for its own internal projets.
Introduction
API on JSON is a Node library that allows the creation of generic REST APIs from a simple JSON configuration file.
The library was initially though to expose simple MongoDB data models through REST APIs. It was designed to use a descriptive rather than imperative approach: basically you describe your MongoDB data structure in a JSON file, the library does the rest.
It then evolved into a general purpose library that supports real time, custom callbacks, access control, etc.
Disclaimer : JSON configuration files are technically JS files. We still call them JSON because at the beginning of the projets we only used JSON files.
Concepts
The library is structured around three main classes: Server, ApiModel and DataModel.
Server creates an ExpressJS server.
ApiModel describes the structure of the API. This is passed to the Server to create API routes.
DataModel is used for the particular (yet common) case where you need to create a API that access data stored in a DB. This class delegates the actual DB access to an external class that implements the vendor specifics CRUD operations. Two DB classes are available : MongoDB and RethinkDB (for real time use cases).
Sister libraries
- upload-models
- accounts-models
- aoj-admin
Full features example
const { DataModel } = require('@appunto/api-on-json');
const { Mongo } = require('@appunto/api-on-json');
const mongoUri = 'http://localhost:27017';
const dataModels = {
'cars': {
schema: {
'brand' : {type : 'String', 'required' : true},
'model' : {type: 'String', 'default' : 'Default Model'},
'speed' : {type: 'Number', 'min': 0, 'max': 300},
'buyable' : {type: 'Boolean'},
'constructor_id' : {type : 'Id', collection : 'constructors'}
},
options: {
searchableFields: ['brand']
}
},
'otherTable': {
schema: {
'color' : {type: 'String'},
'whatever' : [{type : 'String'}]
}
},
'constructors': {
schema: {
'name': {type: "String"},
'models': [
{'car_id': {type: 'Id', collection: 'cars'}}
]
}
}
};
const db = new Mongo(mongoUri, options);
const opt = {
realTime: false
};
const dataModel = new DataModel(dataModels);
await db.connect();
await db.init(dataModel);
const apiModel = dataModel.toApi(opt);
const env = {
db : db,
jwtSecret : "--your-jwt-secret-key--"
}
const server = apiModel.toServer(env);
await server.listen(8081);Installation
npm install @appunto/api-on-jsonRecipes
Simple MongoDB Api
To be documented
Extending Api
References
Server
new Server(apiModel, [environment])
Creates a new Server instance. In practice you will probably prefer to use ApiModel.toServer(...).
Arguments
apiModelis a instance ofApiModelenvironment(optional) is an object that can be used to hold environment variables.environmentobject is passed to all Api handlers (seeApi)
Example
const { Server } = require('@appunto/api-on-json');
const apiModel = new ApiModel(/* see ApiModel doc */);
const environment = {
VARIABLE : 'value'
};
const server = new Server(apiModel, environment);Server.listen(port)
Starts a server.
Arguments
portthe port to which the server listen to
Example
const server = new Server(apiModel, environment);
server.listen(80);Server.close()
Stops a server.
Arguments
Example
const server = new Server(apiModel, environment);
server.listen(80);
server.close();Api
JSON model
JSON Api model describes the behavior of the API.
A JSON Api model is composed by a set of route definitions
const model = {
'/route1' : {/* definition */},
'/route2' : {/* definition */},
};Each route shall begin with a '/'.
Route can contain :
authmultiple authentication rules for each method:requiresAuthboolean, true if an authentication is needed or false if notrequiresRoleslist of string enumerating the roles that can be authenticatedpolicieslist of function called at the server creation
handlersmultiple functions executed for each methodfiltersmultiple functions executed before each methodrealTimemultiple functions needed for real-time api:connectlist of function called at socket connectionmessagelist of function called at socket updatedisconnectlist of function called at socket disconnection
corsmultiple options pass to HelmetJS (see Helmet doc for more information)
Example
const model = {
'/route1' : {
auth : {
"GET" : {requiresAuth:false, requiresRoles:false, policies:[policy1]},
"HEAD" : {requiresAuth:true, requiresRoles:['role1'], policies:[policy1]},
"OPTIONS" : {requiresAuth:true, requiresRoles:false, policies:[policy1]},
"POST" : {requiresAuth:true, requiresRoles:false, policies:[policy1]},
"PUT" : {requiresAuth:true, requiresRoles:false, policies:[policy1]},
"PATCH" : {requiresAuth:true, requiresRoles:false, policies:[policy1]},
"DELETE" : {requiresAuth:true, requiresRoles:false, policies:[policy1]},
realTime : {requiresAuth:true, requiresRoles:false, policies:[policy1]}
},
handlers : {
"GET" : [handler1]
},
filters : {
"POST" : [filter1]
},
realTime : {
"connect" : [connectHandler1, connectHandler2],
"message" : [messageHandler1, messageHandler2],
"disconnect" : [disconnectHandler1, disconnectHandler2]
},
cors : {
methods : "GET, HEAD, PUT, PATCH, POST, DELETE",
optionsSuccessStatus : 204,
origin : "*",
preflightContinue : false
}
}
};new ApiModel(...apiModels)
Creates a new ApiModel instance.
Arguments
apiModels0 or more apiModels, either in JSON or of ApiModel class
Returns
ApiModelReturns the ApiModel instance newly created
ApiModel.get()
Arguments
- None
Returns
- Returns a merged and compiled apiModel of current state
ApiModel.addModel(...apiModels)
Adds apiModels to the ApiModel instance.
Arguments
apiModels0 or more apiModels, either in JSON or of ApiModel class
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.addModel(/* your models */);ApiModel.addRoute(route, definition)
Add a new route or modify current route's definition.
Arguments
routepath to where to add the definitiondefinitiondifferent handlers and authentication rules which indicates how the route is handled
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.addRoute('/route1', {/*...*/});ApiModel.removeRoute(route)
Remove the route in the ApiModel instance.
Arguments
routepath to the location to be removed
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.removeRoute('/route1/subRoute1');ApiModel.addHandler(route, method, handlers)
Adds one or more handlers to the route for the method
Arguments
routepath to the locationmethodmethod in which thehandlerswill gohandlersa function or an array of functions to handle the route
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.addHandler('/users', 'GET', [handler1, handler2]);ApiModel.addFilter(route, method, filters)
Adds one or more filters to the route for the method
Arguments
routepath to the locationmethodthemethodin which thefilterswill gofiltersa function or an array of functions to filter themethodatroute
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.addFilter('/users', 'GET', filter);ApiModel.setAuth(route, auth)
Arguments
routepath to the locationauthdefinition of the authentication to accessroute
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.setAuth('/users', {requiresAuth: true, requiresRoles: ['admin']});ApiModel.setRequiresAuth(route, value)
Arguments
routepath to the locationvalueboolean to able authentication or disable it
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.setRequiresAuth('/users', false);ApiModel.setRequiresRoles(route, roles)
Arguments
routepath to the locationrolesa string or an array of strings to add roles which can access theroute
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.setRequiresRoles('/users', ['admin', 'user']);ApiModel.addPolicies(route, policies)
Arguments
routepath to the locationpoliciesa function or an array of functions which will be called at theServercreation
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.addPolicies('/users', [policy]);ApiModel.addRealTimeHandler(route, realTimeHandlers)
Arguments
routepath to the locationrealTimeHandlersa function or an array of functions which will be called with thesocketin realTime
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.addRealTimeHandler('/users', {connect: connectHandler, disconnect: disconnectHandler1});ApiModel.addConnectHandler(route, connect)
Arguments
routepath to the locationconnecta function or an array of functions which will be called at thesocketconnection in realTime
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.addConnectHandler('/users', connectHandler);ApiModel.addMessageHandler(route, message)
Arguments
routepath to the locationmessagea function or an array of functions which will be called onsocketmessage in realTime
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.addMessageHandler('/users', [messageHandler1]);ApiModel.addDisconnectHandler(route, disconnect)
Arguments
routepath to the locationdisconnecta function or an array of functions which will be called at thesocketdisconnection in realTime
Returns
ApiModelReturns the ApiModel instance
Example
const apiModel = new ApiModel();
apiModel.addDisconnectHandler('/users', [disconnectHandler1, disconnectHandler2]);ApiModel.toServer(env)
Merges and compiles the model to create a Server from an ApiModel instance.
Arguments
envobject which contains if needed the db and a secret key for JWT
Returns
ServerReturns the server created
Example
const server = apiModel.toServer(environment);
server.listen(80);
server.close();Data
JSON data model
JSON Data model describes the shape of the data in the database.
A JSON Data model is composed by a set of schema and options.
const model = {
'collection1' : {
schema: {/* definition */},
options: {/* definition */}
},
'collection2' : {
schema: {/* definition */},
options: {/* definition */}
}
};Each collection can contain a schema and some options.
A schema is composed of multiple fields which are made from a name and some specifications
Specifications can contain:
typethe type of the field, it can be:Stringindicates that the field is a stringNumberindicates that the field is a number, integer or floatDateindicates that the field is a dateBooleanindicates that the field is a boolean, either true or falseIdindicates that the field is an id pointing to another collectionMixedindicates that the field is not one of the above
For all types:
requiredboolean which indicates either this field is required or not for validationdefaultan object of the same type that intype, use when no value is set for this fielduniqueindicates that this field can't have duplicate valueindexindicates that the field can be used as an index in the database
For
String:lowercaseapply toLowerCase() to the string valueuppercaseapply toUpperCase() to the string valuetrimapply trim() to the string valuematchadd a validator to the string value and a regexminlengthadd a minimum validator to the string lengthmaxlengthadd a maximum validator to the string length
- For
Number:min: indicates the minimum valid numbermax: indicates the maximum valid number
- For
Date:min: indicates the minimum valid datemax: indicates the maximum valid date
- For
Id:collection: string which indicates the collection in which the id is pointing
Options can contain :
timestampsallows to disable or modify the datescreatedAtandupdatedAtof the datatypeKeyindicates the key used astypein the schema.searchableFieldsan array of collection's name that are searchable. An empty array means you can't do research in the database.
Example
const dataModels = {
'collection1': {
schema: {
'field1' : {type : 'String', 'required' : true},
'field2' : {type: 'String', 'default' : 'Default Model'},
'field3' : {type: 'Number', 'min': 0, 'max': 300},
'field4' : {type : 'Id', collection : 'collection3'}
},
options: {
searchableFields: ['field1']
}
},
'collection2': {
schema: {
'field1' : {type: 'String'},
'field2' : [{type : "String"}]
}
},
'collection3': {
schema: {
'field1': {type: "String"},
'field2': [
{'subField1': {type: 'Id', collection: 'cars'}}
]
}
}
};new DataModel(...dataModels)
Creates a new DataModel instance.
Arguments
dataModels0 or more dataModels, either in JSON or from DataModel class
Returns
DataModelReturns the DataModel instance newly created
Example
const dataModel = new DataModel(/* your models */);DataModel.get()
Arguments
- None
Returns
- Returns a merged and compiled data model of current state
Example
const dataModel = new DataModel(/* your models */);
const merged = dataModel.get();DataModel.addModel(...dataModels)
Adds data models to the DataModel instance.
Arguments
dataModels0 or more data models, either in JSON or in DataModel class
Returns
DataModelReturns the DataModel instance
Example
const dataModel = new DataModel(/* your models */);
dataModel.addModel(/* your models */);DataModel.addCollection(collection, definition)
Adds a new collection to the DataModel instance.
Arguments
collectionthe name of the collectiondefinitionthe fields of the collection
Returns
DataModelReturns the DataModel instance
Example
const dataModel = new DataModel();
dataModel.addCollection('cars', {
schema: {
'model' : { type: 'String' }
},
options: {
searchableFields: ['model']
}
});DataModel.addField(collection, field, definition)
Adds a new field at collection in the DataModel instance.
Arguments
collectionthe name of the collection in which the field will be addedfieldthe name of the fielddefinitionthe specifications of the field
Returns
DataModelReturns the DataModel instance
Example
const dataModel = new DataModel();
dataModel.addField('cars', 'brand', {type: String, required: true});DataModel.removeCollection(collection)
Removes a collection in the DataModel instance.
Arguments
collectionthe name of the collection to be removed
Returns
DataModelReturns the DataModel instance
Example
const dataModel = new DataModel();
dataModel.removeCollection('cars');DataModel.removeField(collection, field)
Removes a collection in the DataModel instance.
Arguments
collectionthe name of the collection in which the field will be removedfieldthe name of the field to be removed
Returns
DataModelReturns the DataModel instance
Example
const dataModel = new DataModel();
dataModel.removeField('users', 'old_username');DataModel.setOptions(collection, options)
Sets the options of the collection in the DataModel instance.
If the collection doesn't exist it creates it.
Arguments
collectionthe name of the collectionoptionsthe options to be set
Returns
DataModelReturns the DataModel instance
Example
const dataModel = new DataModel();
dataModel.setOptions('users', {searchableFields: ['username', 'name']});DataModel.setType(collection, field, type)
Sets the type of a field in the given collection in the DataModel instance.
Arguments
collectionthe name of the collectionfieldthe name of the fieldtypethe new type of the field
Returns
DataModelReturns the DataModel instance
Example
const dataModel = new DataModel();
dataModel.setType('users', 'username', 'String');DataModel.setRequired(collection, field, value)
Sets if the field in collection is required or not in the DataModel instance.
Arguments
collectionthe name of the collectionfieldthe name of the fieldvalueboolean, true if it is required, false if it is not required
Returns
DataModelReturns the DataModel instance
Example
const dataModel = new DataModel();
dataModel.setRequired('users', 'username', true);DataModel.setUnique(collection, field, value)
Sets if the field in collection is unique or not in the DataModel instance.
Arguments
collectionthe name of the collectionfieldthe name of the fieldvalueboolean, true if it is unique, false if it is not unique
Returns
DataModelReturns the DataModel instance
Example
const dataModel = new DataModel();
dataModel.setUnique('users', 'username', true);DataModel.toApi(opt)
Merges and compiles the model to create an ApiModel from a DataModel instance.
Arguments
optobject which contains if needed the collections with realTime
Returns
ApiModelReturns the ApiModel created
Example
const apiModel = dataModel.toApi({realTime: ['collection1', 'collection3']});Database
In api-on-json you can add the handling of your database.
By default the library handle MongoDB and RethinkDB.
You can add a DB of your own, you just have to create a new DB class and write those methods:
connecthandle the connection with your dbinitinit the list of data modelscreatefor POST requestremovefor DELETE requestreadOnefor GET id requestreadManyfor GET request with queriesupdatefor PUT requestpatchfor PATCH requestobservefor realTime (if your db handle it)
Mongo
The class which handles mongoDB Api
Example
const db = new Mongo(/*mongoUri*/, options);Rethink
The class which handles rethinkDB Api
Example
const db = new Rethink("localhost", "28015", "G");How to create a new database class
Later on, you might want to use your favorite database for your api, and if we didn't implement it, you will need to add it yourself!
But don't worry, here is how to do it!
First Step: Make a constructor
First you will need a constructor to create your db instance
You will need 3 things:
- all the parameters you need to connect to your db (an url, options ...)
- a database field where you will put your db object (for mongoDB: mongoose, for rethinkDB: r)
- a models list we will use it later, this will be the array where all the models used in your api will be
class YourDB {
constructor(url, options) {
this.url = url;
this.options = { ...options};
this.database = null;
this.models = [];
}
}Second Step: Make a connect method
After that you will need a connect method, called by our library when the dataModel is compiled.
You should set this.database to its value (return by the connection call of the database you are using).
Third Step: Make an init method
You will also need an init method, called by our library just after connect. Here the goal is to setup your database for the dataModel you have created before. That means if needed create:
- tables
- collection
- schema
- etc
Fourth Step: Create all CRUD actions
The final step and the hardest, you will have to create each of the following callbacks.
All are required (except for observe which handles the realTime).
create(collection, data)Your create method is the one called on a POST request to your api.
You will need to get the model corresponding to the collection first. Then verify that the data you received is according to your model (i.e type, options etc). If all was validate you can then add it to your database. If all went fine, you can return the JSON of the new entry added to your db.
remove(collection, id)Your remove method is the one called on a DELETE request to your api.
You only need to request a deletion of the entry at the given
idin the database.readOne(collection, id)Your readOne method is the one called on a GET request with an id to your api.
You need to return the entry at the given
idin the database.readMany(collection, query)Your readMany method is the one called on a simple GET request or on GET with query request to your api.
You will first need to parse the query parameter. You will find different object.
pageandpageSizeare used for pagination, the number of page is determined by the number of elements in the db and the pageSize.sortis a string looking like 'field,order' or an array of those string. The results returned by readMany should be sorted according to those strings, by field and either in descending or ascendingorder(ascending by default).cursoris used for pagination too, it is the lastidor a combination of the lastidand the sorting method used at the previous GET request.qis used for searching in the db all elements that have a field that match a value. For example all elements that have at least one field starting with 'Tar'. We would get 'Tartine', 'Tarami' etc. Note that you only have to search in the searchableFields, if the user wants to research an other fields he should use thefparameter described after.fallows to have all values between a min and a max, it's an array of afieldName, acomparatorfrom this list 'lt, le, gt, ge' and avalthat will be the bound.update(collection, id, data)Your update method is the one called on a PUT request.
You will need to get the model corresponding to the collection first. Then verify that the data you received is according to your model (i.e type, options etc). If all was validate, you will have to replace the existing element in the db with the new one. Then return the newly created element.
patch(collection, id, data)Your update method is the one called on a PATCH request.
You will need to get the model corresponding to the collection first. Then verify that the data you received is according to your model (i.e type, options etc). If all was validate, you will have to change the existing element in the db with only the fields in data. Meaning that it should not raise an error if a required field is omit in data, because this field will just keep its previous value Then return the newly created element.
observe(collection, query, params, socket, callback)This is the method use to put an observer to your db when you are using the realTime. Note that all database are not compatible with that features. In this method you will have to set up the way your realtime works (for example changefeeds in rethinkDB). Then the callback you will use is here to pair the socket for realTime.
See observe in
Rethinkclass for more information.
Tests
Different tests exist, to run the tests suite:
npm run testYou can also run a single test file with:
npm run single-test ${path}10 months ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
6 years ago