Security News
Fluent Assertions Faces Backlash After Abandoning Open Source Licensing
Fluent Assertions is facing backlash after dropping the Apache license for a commercial model, leaving users blindsided and questioning contributor rights.
rest-resource
Advanced tools
REST Resource is a library that makes your life simpler when working with REST API endpoints. It takes RESTful Resource/Service URIs and simplifies them into a Class that can be called with various built-in methods. Think of it like a Model for REST API Endpoints.
await comment.resolveAttribute('post.author.city')
// GET /posts/1
// GET /users/1
// => San Francisco, CA
REST is acronym for REpresentational State Transfer. It is architectural style for distributed hypermedia systems and was first presented by Roy Fielding in 2000 in his famous dissertation.
Like any other architectural style, REST also does have it’s own 6 guiding constraints which must be satisfied if an interface needs to be referred as RESTful.
Please read further documentation
npm install rest-resource
or
<script src="https://unpkg.com/rest-resource"></script>
Please see Documentation
npm run serve-docs
Play around with a working example on CodePen
(assuming Node >= v12
)
Given the URIs:
/users
/users/<id>
/roles
/roles/<id>
/groups
/groups/<id>
Code:
import Resource from 'rest-resource'
class UserResource extends Resource {
static endpoint = '/users'
greet() {
console.log('I am %s, %s!', this.attributes.name, this.attributes.role)
}
}
const arthur = await UserResource.detail(123)
// GET /users/123
arthur.greet()
// => I am Arthur, King of the Britons!
// Or, get many resources
const users = await UserResource.list()
// GET /users
let robin = users.resources[0]
robin.greet()
// => I am Brave Sir Robin, Knight of the Round Table!
console.log(robin.id)
// => 456
robin.set('weapon', 'Sword')
robin.save()
// PATCH { weapon: 'Sword' } to /users/456
REST Resource has a caching mechanism that sorts out which resources your app needs and satisfies both requirements once that resource has been obtained, provided they're made within n
seconds where n
is a Resource's cacheMaxAge
. Resource retrieval can be defined synchronously or asynchronously and REST Resource will still only need to make the initial request.
In the example below, only one request is made to /users/123
, even though the data from that endpoint is required twice:
let user = await UserResource.detail(123)
// GET /users/123
user.greet()
// => I am Arthur, King of the Britons!
console.log(user === await UserResource.detail(123))
// => true
If an object has been retrieved and is called upon from other related models, REST Resource will resolve cross-referenced lookups, provided they're made within n
seconds where n
is a Resource's cacheMaxAge
. In the example below, notice the groups
attribute contains an already-retrieved Group Resource:
let arthur = await UserResource.detail(123)
console.log(arthur.attributes)
// => {
// id: 123,
// name: 'King Arthur',
// weapon: 'Sword',
// role: 1,
// groups: [1]
// }
let patsy = await UserResource.detail(654)
console.log(patsy.attributes)
// => {
// id: 654,
// name: 'Patsy',
// weapon: 'Coconuts',
// role: 2,
// groups: [1, 2] // Notice this list contains an already retrieved group (1)
// }
let arthurTitle = arthur.resolveAttribute('role.title')
let arthurGroup = arthur.resolveAttribute('groups.name')
// GET /roles/1
// GET /groups/1
let patsyTitle = patsy.resolveAttribute('role.title')
let patsyGroup = patsy.resolveAttribute('groups.name')
// GET /roles/2
// GET /groups/2
// Does not need to GET /groups/1
Changing cache lifespan:
class CustomResource extends Resource {
static cacheMaxAge = 60 // Defaults to 10 seconds
// ...
}
Turning cache off:
class NoCacheResource extends Resource {
static cacheMaxAge = -1
// ...
}
Related Resources can be wired up very easily.
import Resource from 'rest-resource'
class RoleResource extends Resource {
static endpont = '/roles'
}
class UserResource extends Resource {
static endpoint = '/users'
static related = {
role: RoleResource
}
}
let user = await UserResource.detail(321, { resolveRelated: true }) // Add resolveRelated: true here and it'll automatically resolve related resources
// GET /users/321
// GET /roles/<id>
// Using get() gets the attribute `title `on related key `role`
let title = user.get('role.title')
let name = user.get('name')
console.log('%s the %s!', name, title)
// => Tim the Enchanter!
console.log(user.get('role'))
// => RoleResource({ id: 654, title: 'Enchanter' })
Many to many relationships work exactly the same as One to Many relationships with one key difference: when using resource.get(attribute)
where attribute
is the field of a related resource, the returned value is not the related Resource
instance, it's a RelatedManager
instance:
class UserResource extends Resource {
static endpoint = '/users'
static related = {
role: RoleResource,
groups: GroupResource // Many to many relationship
}
}
let user = await UserResource.detail(654)
console.log(user.attributes)
// => {
// id: 654,
// name: 'Patsy',
// weapon: 'Coconuts',
// role: 2,
// groups: [1, 2]
// }
let manager = user.rel('groups') // Many to many relationship
console.log(manager.resolved)
// => false
// REST Resource doesn't automatically resolve related lookups unless instructed by using resolveRelated
await manager.resolve()
// GET /groups/1
// GET /groups/2
console.log(manager.resolved)
// => true
console.log(manager.resources)
// => [GroupResource, GroupResource]
resolveRelated()
and { resolveRelated: true }
When using ResourceClass.detail()
and ResourceClass.list()
, one of the available options is { resolveRelated: true }
, which will automatically resolve related resources.
{ resolveRelated: true }
with ResourceClass.detail()
:let user = await UserResource.detail(654, { resolveRelated: true })
// GET /users/654
// GET /roles/2
// GET /groups/1
// GET /groups/2
console.log('groups.name')
// => ["Some Group", "Another Group"]
{ resolveRelated: true }
with ResourceClass.list()
:let users = await UserResource.list({ resolveRelated: true })
// GET /users
// GET /roles/1
// GET /roles/2
// GET /groups/1
// GET /groups/2
resourceInstance.resolveRelated()
:let user = await UserResource.detail(654)
// GET /users/654
await user.resolveRelated()
// GET /roles/2
// GET /groups/1
// GET /groups/2
Additionally, you can also provide a list of managers that you want to resolve:
let user = await UserResource.detail(654)
// GET /users/654
await user.resolveRelated(['role']) // Will ignore all but "role" field (notice the "groups" were not resolved)
// GET /roles/2
{ resolveRelatedDeep: true }
To resolve all attributes recursively, you can provide { resolveRelatedDeep: true }
instead. This will retrieve all related resources, and any other related resources attached to those resources. Be careful when using this option as it may cause performance issues.
resolveAttribute()
REST Resource automatically resolves properties from related lookups, then decides what fields it needs to call resource.resolveRelated()
let roger = await UserResource.detail(543)
// GET /users/543
let title = await roger.resolveAttribute('role.title')
// GET /roles/<id>
console.log(title)
// => Shrubber
resource.resolveAttribute(attribute)
is one of the most useful features of REST Resource as it allows you to define the fields that are necessary to build your app, and REST Resource will intelligently request only the data it needs from the API!Resource.related
as a functionYou can also define related resources with a function. This is useful when lazy loading modules or resolving circular dependencies:
class UserResource extends Resource {
static endpoint = '/users'
static related() {
return {
role: require('./resources/role').default
}
}
}
The response from the API might be returning nested objects. For example:
// GET /posts/1
{
"title": "Nullam nibh enim, ultrices quis",
"body": "Pellentesque ultrices augue eleifend augue posuere pretium. Nulla sodales turpis eget pretium efficitur.",
"user": {
"id": 123,
"name": "King Arthur",
"weapon": "Sword",
"role": 1,
"groups": [1]
}
}
In the response body above, the post
object contains a nested user
object that you may want to cache. You can do so by setting nested: true
when defining a relationship.
class PostResource extends Resource {
static endpoint = '/posts'
static related = {
user: {
to: UserResource,
nested: true
}
}
}
You can use REST Resource like a RESTful Model. REST Resource tracks changes in each Resource instance's attributes and uses RESTful HTTP Verbs like GET
, POST
, PUT
, PATCH
, and DELETE
accordingly:
let robin = new UserResource({
name: 'Brave Sir Robin',
weapon: 'Sword'
})
await robin.save()
// POST /users
let knight = new RoleResource({ title: 'Knight of the Round Table' })
await knight.save()
// POST /roles
robin.set('role', knight.id)
robin.save()
// PATCH /users/<robin-id>
robin.update()
// GET /users/<robin-id>
robin.runAway()
robin.delete()
// DELETE /users/<robin-id>
You can also specify the defaults that a Resource instance should have:
class UserResource extends Resource {
static endpoint = '/users'
static defaults = {
name: 'Unknown User',
weapon: 'Hyperbolic Taunting',
groups: [1]
}
}
let unknown = new UserResource({ groups: [2] })
console.log(unknown.get('name'))
// => Unknown User
You can use Resource.extend
to define static members. For example:
const UserResource = Resource.extend({
endpoint: '/users',
related: {
// ...etc
}
})
Client
ClassThe Client class is a simple request library that is meant to be customized to suit your needs.
For requests, REST Resource uses a basic Axios client (JWTBearerClient
class in src/client
). You can override this by creating a custom client.
For more information on how Axios works, please refer to Axios documentation
When creating a custom client, you can override any methods you'd like. One method in particular you'll need to focus on is negotiateContent()
which should return a function that parses the body of the response into a list of objects.
There are a number of ways you can set the client:
import Resource from 'rest-resource'
import { DefaultClient } from 'rest-resource/dist/client'
class CustomClient extends DefaultClient {
negotiateContent(ResourceClass) {
return (response) => {
let objects = []
if(Array.isArray(response.data)) {
response.data.forEach((attributes) => objects.push(new ResourceClass(attributes)))
} else {
objects.push(new ResourceClass(response.data))
}
return {
response,
objects,
pages: () => response.headers['Pagination-Count'],
currentPage: () => response.headers['Pagination-Page'],
perPage: () => response.headers['Pagination-Limit'],
}
}
}
}
class CustomResource extends Resource {
static client = new CustomClient('http://some-api.com')
}
// Or: (this method can also be used to override client globally by replacing "CustomResource" with "Resource")
CustomResource.client = new CustomClient('http://some-api.com')
Whenever the user wants to access a protected route or resource, the user agent should send the JWT, typically in the Authorization header using the Bearer schema. The content of the header should look like the following:
Authorization: Bearer <token>
For more information on how JWTs work, please see JSON Web Token Documentation
Whenever a related field is defined, a manager is created to that field. You can customize this class by extending it and assigning it when you create a class.
import Resource from 'rest-resource'
import RelatedManager from 'rest-resource/dist/related'
class CustomRelatedManager extends RelatedManager {
batchSize: 50 // Only GET 50 related objects at a time (default: Infinity)
/**
* @param options Object (resolveRelated, etc.)
* @returns Resource[] List of Resource instances
*/
resolve(options) {
// etc
}
}
class UserResource extends Resource {
static endpoint = '/users'
// Define custom related manager here
static RelatedManagerClass = CustomRelatedManager
}
You can specify a validation object literal as a static member of any Resource class and run validations against any attribute. Validations are ran automatically when using resource.save()
. Note: validation functions should throw exceptions.ValidationError(field, message)
import Resource from 'rest-resource'
function phoneIsValid(value, instance, ValidationError) {
let regExp = /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/im
if(!regExp.test(value)) {
throw new ValidationError('phone')
}
}
function isFiveChars(value, instance, ValidationError) {
if(String(value).length < 5) {
throw new ValidationError('', 'This field must be at least 5 characters')
}
}
function isAlphaNumeric(value, instance, ValidationError) {
let regExp = /^[a-z0-9]+$/i
if(!regExp.test(value)) {
throw new ValidationError('', 'This field must be alphanumeric')
}
}
class UserResource extends Resource {
static endpoint = '/users'
static validation = {
phone: phoneIsValid,
username: [
isFiveChars,
isAlphaNumeric
]
}
}
let user = await UserResource.detail(1)
user.set('phone', '415-555-1234')
user.validate()
// => []
user.set('phone', '555555x1234')
user.validate()
// => [{ ValidationError: phone: This field is not valid }]
class UserResource extends Resource {
static endpoint = '/users'
static validation() {
return {
phone: phoneIsValid,
username: [
isFiveChars,
isAlphaNumeric
]
}
}
}
Whenever an attribute is defined, you can normalize its value into a desired output (think of it as a unidirectional serializer). The built-in normalizers are:
StringNormalizer
NumberNormalizer
BooleanNormalizer
CurrencyNormalizer
Attribute Normalizers can ensure that the values assigned to resource.attributes[key]
(and subsequently the data being sent to the API) is consistent.
Attribute Normalization is particularly useful when trying to normalize inconsistent data from the API. The example below represents two responses from the API, one representing a comment
object and a post
object. Both objects are referencing a user
. However, the value of user
on each comment
and post
objects are inconsistent (one is a primary key, and one is an nested user
object):
// GET /comments/1
{
"content": "Nullam vel ultrices eros. Nullam at metus venenatis, bibendum lectus sed",
"post": 1,
"user": 123
}
// GET /posts/1
{
"title": "Nullam nibh enim, ultrices quis",
"body": "Pellentesque ultrices augue eleifend augue posuere pretium. Nulla sodales turpis eget pretium efficitur.",
"user": {
"id": 123,
"name": "King Arthur",
"weapon": "Sword",
"role": 1,
"groups": [1]
}
}
You can use attribute normalization to solve this issue
import Resource from 'rest-resource'
import { NumberNormalizer } from 'rest-resource/dist/helpers/normalization'
class PostResource extends Resource {
static endpoint = '/posts'
static related = {
user: UserResource
}
static normalization = {
user: new NumberNormalizer()
}
}
class CommentResource extends Resource {
static endpoint = '/comments'
static related = {
user: UserResource,
post: PostResource
}
static normalization = {
user: new NumberNormalizer(),
}
}
let post = await PostResource.detail(1)
console.log(post.attributes.user)
// => 123
let comment = await CommentResource.detail(1)
console.log(comment.attributes.user)
// => 123
You can also specify custom normalizer
class PostResource extends Resource {
static endpoint = '/posts'
static related = {
user: UserResource
}
static normalization = {
user: {
normalize: (value) => {
// Normalize and return new value
}
}
}
}
You can also use the normalizerFactory(normalizerClassName)
helper instead of importing all normalizer classes:
import { normalizerFactory } from 'rest-resource/dist/helpers/normalization'
class PostResource extends Resource {
static endpoint = '/posts'
static related = {
user: UserResource
}
static normalization = {
user: normalizerFactory('NumberNormalizer')
}
}
You can wrap routes on the Resource's endpoint by using wrap(path, query)
methods on the class constructor and its prototype. Once you define the wrapped endpoint, you can get(options)
, post(data, options)
, put(data, options)
, etc.
let wrappedRequest = PostResource.wrap('/pending')
let comments = await wrappedRequest.get()
// GET /posts/pending
let firstPost = await PostResource.detail(1)
// GET /posts/1
let wrappedRequest = firstPost.wrap('/comments')
let comments = await wrappedRequest.get()
// GET /posts/1/comments
You can provide a query on wrap(path, query)
methods
let firstPost = await PostResource.detail(1)
// GET /posts/1
await firstPost.wrap('/comments', { approved: true, user: 1 }).get()
// GET /posts/1/comments?approved=true&user=1
let firstPost = await PostResource.detail(1)
// GET /posts/1
let approveEndpoint = firstPost.wrap('/approve')
await approveEndpoint.post({ approverId: 1 })
// POST /posts/1/approve
// {
// approverId: 1
// }
Note: If the instance of the Resource does not have an ID (eg. is not saved), an error will be thrown:
let newPost = new PostResource({ user: 1, title: "sunt aut facere repellat" })
newPost.wrap('/comments') // AssertionError!! (Resource is not saved)
Please see Documentation
npm run serve-docs
FAQs
Simplify your REST API resources
The npm package rest-resource receives a total of 35 weekly downloads. As such, rest-resource popularity was classified as not popular.
We found that rest-resource demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Fluent Assertions is facing backlash after dropping the Apache license for a commercial model, leaving users blindsided and questioning contributor rights.
Research
Security News
Socket researchers uncover the risks of a malicious Python package targeting Discord developers.
Security News
The UK is proposing a bold ban on ransomware payments by public entities to disrupt cybercrime, protect critical services, and lead global cybersecurity efforts.