Javascript Service Model
Core library for handling REST service requests with caching, aggregation and model definitions.
Framework integrations:
Features
- Define models and easily handle REST service requests
- Pass model data to REST service requests and retrieve model data from them
- Aggregation for multiple parallel requests to the same url to avoid redundant requests. See aggregation
- Caches response from services
- Uses axios for service request
- ... more later
Content
- Installation
- Example
- Usage
- Future
- Contribution
- License
Installation
npm install js-service-model
Example
Definition of a simple ServiceModel
without using fields. https://jsonplaceholder.typicode.com/albums/ is being used as an example REST JSON service.
// Define service url static urls = BASE: 'https://jsonplaceholder.typicode.com/albums/'
Retrieve data from a service. objects.detail()
will return a model instance. objects.list()
will return a list of model instances.
// Retrieve all albums from /albums/const allAlbums = await Albumobjects // Retrieve specific album from /albums/1/const album = await Albumobjects // Retrieve filtered list from /albums/?userId=1const userAlbums = await Albumobjects // Create new albumawait Albumobjects // Update an albumawait Albumobjects // Delete specific albumawait Albumobjects
You can easily access the data from a model instance or define model fields.
albumdata
Usage
BaseModel
A BaseModel
can be used to handle data from any source by passing the data when instantiating the model.
// Definition of model fields (optional) static fieldsDef = title: const obj = title: 'My title'
Retrieve data of the model instance.
objdata // output: {title: 'My title'}
Retrieve a list of all fields.
objfields
Retrieve value from a single field.
// Retrieve value for field 'title'await objvaltitle // output: My title
Set value of a field
objvaltitle = 'New title'
Retrieve field instance of given field name
obj
ServiceModel
A ServiceModel
extends from BaseModel
and adds the ModelManager
with a caching
store to keep track of aggregation of running requests and optionally caching the result of the services.
static urls = BASE: 'https://jsonplaceholder.typicode.com/albums/' // Duration to cache requested data in seconds. 0: no cache. null: Cache forever. Default is 30 seconds static cacheDuration = 5 static fieldsDef = id: title:
Urls
Urls are currently divided into 2 different types. LIST
and DETAIL
(same like in Django REST framework).
LIST
: (e.g./albums/
) used forobjects.list()
DETAIL
: (e.g./albums/1/
) used forobjects.detail(1)
The simplest way to define the urls is to set the static property urls.BASE
in your ServiceModel
.
static urls = BASE: 'https://jsonplaceholder.typicode.com/albums/'
When performing a detail request your key will be automatically appended to the end of the BASE
url.
You can also define the LIST
and DETAIL
url separately:
static urls = LIST: 'https://jsonplaceholder.typicode.com/albums/' // {pk} will be replaced with your value you provide to objects.detail() DETAIL: 'https://jsonplaceholder.typicode.com/albums/{pk}/'
There are currently 3 ways to define your url with the following priority
- Overwrite
getListUrl
orgetDetailUrl
method and a return aPromise
which will resolve the url as a string - Set the
LIST
orDETAIL
url in your modelstatic urls = { LIST: <...>, DETAIL: <...> }
- Set the
BASE
url in your modelstatic urls = { BASE: <...> }
If you got a nested RESTful service structure (e.g. /albums/1/photos/
) have a look at parents.
Aggregation
When you start to request data from a service, for example Album.objects.detail('1')
, then the Promise
of the request will
be saved as long as the request has not been completed. So when requesting Album.objects.detail('1')
again (e.g from another component)
this request will be attached to the first request which has not been completed yet and the request of the service will only made once.
In case you want to avoid the request aggregation for a specific request see noRequestAggregation
in ModelManager RetrieveInterfaceParams.
Cache
With the static property cacheDuration
it is possible to set the duration (in seconds) of how long the result of a response
should be cached. The default value is 30 seconds. Currently the expired data will only be removed by requesting the same data again.
- null: cache will not be removed
- 0: no caching
You can manually clear the complete cache including aggregation by calling model.store.clear()
.
In case you want to set cache options for a specific request see ModelManager RetrieveInterfaceParams.
Parents
When using a nested RESTful service more information is necessary to uniquely identify a resource. You need to define parents
in your ServiceModel
.
... // Define name of parents static parents = 'album' static urls = // Add placeholder for parent in your url BASE: 'https://jsonplaceholder.typicode.com/albums/{album}/photos/' // Retrieve all photos from album 1: /albums/1/photos/const photos = await Photoobjects // Retrieve photo 2 from album 1: /albums/1/photos/2/const photo = await Photoobjects
It is necessary to set exact parents otherwise a warning will be printed to the console. You can also add some custom
validation of the parents by extending the checkServiceParents
method of your ServiceModel
. This will be called on default ModelManager
interfaces and when retrieving the service url from getListUrl
or getDetailUrl
.
Fields
Fields will be one of the main features of this library.
fieldsDef
)
Field definition (You can set your model fields with the static property fieldsDef
with a plain object with your fieldname as key and the field instance as value.
Nested fieldsDef
is currently not supported.
... static fieldsDef = first_name: last_name: const myObj = first_name: 'Joe' last_name: 'Bloggs' await myObjvalfirst_name // output: Joeawait myObjvallast_name // output: Bloggs
attributeName
)
Attribute name (By default the key of your field in your fieldsDef
(e.g. first_name
) will be used to retrieve the value from the model data.
You can also set the attributeName
when instantiating the field. It is also possible to access nested data when using dot-notation in attributeName
.
If you need a more specific way to retrieve the value of a field from your data then have a look at Custom/Computed fields.
... static fieldsDef = name: attributeName: 'username' address_city: attributeName: 'address.city' address_street: attributeName: 'address.street.name' const myObj = username: 'joe_bloggs' address: city: 'New York' street: name: 'Fifth Avenue' await myObjvalname // output: joe_bloggsawait myObjvaladdress_city // output: New Yorkawait myObjvaladdress_street // output: Fifth Avenue
label
, hint
)
Field label and hint (With the label
property you can set a descriptive name for your field. hint
is used to provide a detail description of your field. label
and hint
can either be a string
or a function
which should return a string
or a Promise
.
You can access your label/hint with the label
/hint
property of your field instance which will always return a Promise
.
... static fieldsDef = first_name: label: 'First name' 'First name of the employee' ... const firstNameField = myObj await firstNameFieldlabel // output: First nameawait firstNameFieldhint // output: First name of the employee
Field API
Custom/Computed fields
In case you want to define your own field class you just need to extend from Field
. By overwriting the valueGetter
method you are able to map the field value by yourself and create computed values.
{ return data ? datafirst_name + ' ' + datalast_name : null } ... static fieldsDef = full_name: const myObj = first_name: 'Joe' last_name: 'Bloggs' await myObjvalfull_name // output: Joe Bloggs
Field types
Different field types will be added with future releases.
objects
)
ModelManager (The ModelManager
provides the interface to perform the api requests. At the moment there are 2 default interface methods.
objects.list()
)
Retrieve list of data (objects.list()
is used to request a list of data (e.g. /albums/
) and will return a list of model instances.
You can optionally set RetrieveInterfaceParams
as only argument.
The method will use getListUrl
, sendListRequest
and mapListResponseBeforeCache
which can be overwritten for customization.
Examples:
Albumobjects // Request: GET /albums/Photoobjects // Request: GET /albums/1/photos/Albumobjects // Request: GET /albums/?userId=1
objects.detail()
)
Retrieve single entry of data (objects.detail()
is used to request a single entry (e.g. /albums/1/
) and will return a model instance.
The first argument is the primary key which can either be a string
or number
. You can optionally set RetrieveInterfaceParams
as second argument.
The method will use getDetailUrl
, sendDetailRequest
and mapDetailResponseBeforeCache
which can be overwritten for customization.
Examples:
Albumobjects // Request: GET /albums/1/Photoobjects // Request: GET /albums/1/photos/5/
objects.create()
)
Create single entry (objects.create()
is used to create a single entry under (e.g. /albums/
) by sending a request with method POST
.
You can provide your data you want to send with post as first argument. The method will use getListUrl
and sendCreateRequest
.
Examples:
Albumobjects // Request: POST /albums/Photoobjects // Request: POST /albums/1/photos/
objects.update()
)
Update single entry (objects.update()
is used to update a single entry under (e.g. /albums/1/
) by sending a request with method PUT
.
The first argument is the primary key which can either be a string
or number
. You can provide your data you want to send with put as first argument.
The method will use getDetailUrl
and sendUpdateRequest
.
Examples:
Albumobjects // Request: PUT /albums/1/Photoobjects // Request: PUT /albums/1/photos/5/
objects.delete()
)
Delete single entry (objects.delete()
is used to delete a single entry under (e.g. /albums/1/
) by sending a request with method DELETE
.
The method will use getDetailUrl
and sendDeleteRequest
.
Examples:
Albumobjects // Request: DELETE /albums/1/Photoobjects // Request: DELETE /albums/1/photos/5/
RetrieveInterfaceParams
With RetrieveInterfaceParams
you can provide additional parameters for objects.list()
and objects.detail()
e.g. for using query parameters or parents.
Full structure example:
// Optional service parents to handle nested RESTful services parents: album: 1 // Filter params as plain object which will be converted to query parameters (params in axios) filter: userId: 1 // Do not use and set response cache. Requests will still be aggregated. Already cached data will not be cleared // Optional: default = false noCache: false // Do not use request aggregation. Response will still be set and used from cache // Optional: default = false noRequestAggregation: false // Cache will not be used but set. Requests will still be aggregated // Optional: default = false refreshCache: false
Exceptions
Error codes from response (e.g. 401 - Unauthorized) will be mapped to an APIException
. You can catch a specific error by checking with instanceof
for your required exception.
... try albums = await Albumobjects catch error if error instanceof UnauthorizedAPIException // Unauthorized else if error instanceof APIException // Any other HTTP error status code else // Other exceptions throw error
All API exceptions inherit from APIException
and contain the response as property (error.response
).
HTTP status code | Exception |
---|---|
400 - Bad Request | BadRequestAPIException |
401 - Unauthorized | UnauthorizedAPIException |
403 - Forbidden | ForbiddenAPIException |
404 - Not Found | NotFoundAPIException |
500 - Internal Server Error | InternalServerErrorAPIException |
Other | APIException |
Custom ModelManager
You can extend the ModelManager
and add your own methods.
... static ModelManager = { const Model = thismodel return title: 'Custom Album' } const customAlbum = Albumobjects
It is also possible to overwrite some methods to do the list
/detail
request by yourself or map the response data before it gets cached and used for the model instance.
sendListRequest
- Gets called when doing a list request with
objects.list()
- Gets called when doing a list request with
sendDetailRequest
- Gets called when doing a detail with
objects.detail()
- Gets called when doing a detail with
sendCreateRequest
- Gets called when sending a create with
objects.create()
- Gets called when sending a create with
sendUpdateRequest
- Gets called when sending a update with
objects.update()
- Gets called when sending a update with
sendDeleteRequest
- Gets called when sending a delete with
objects.delete()
- Gets called when sending a delete with
buildRetrieveRequestConfig
- Gets called from
sendListRequest
andsendDetailRequest
and usesRetrieveInterfaceParams
to return the request configuration for axios.
- Gets called from
mapListResponseBeforeCache
- Gets called from
sendListRequest
with the response data before the data will be cached
- Gets called from
mapDetailResponseBeforeCache
- Gets called from
sendDetailRequest
with the response data before the data will be cached
- Gets called from
handleResponseError
- Receives Errors from
axios
and maps it to api exceptions
- Receives Errors from
Future
- Models
- Cache
- Define a different cacheDuration for a specific request
- Use cache from list response also for detail requests
- "garbage collector" to remove expired cache
- Cache
- Fields
- Different field types
- Standalone field instances
- Accessing foreign key fields and retrieving foreign model instances
- Global configuration with hooks
- ...
Contribution
Feel free to create an issue for bugs, feature requests, suggestions or any idea you have. You can also add a pull request with your implementation.
It would please me to hear from your experience.
I used some ideas and names from django, django REST framework, ag-Grid and other libraries and frameworks.