Rest Client JS SDK
Rest client SDK for API for Javascript usage.
This client tries to avoid the complexity of implementing a custom SDK for every
API you have. You just have to implements your model and a little configuration
and it will hide the complexity for you.
Installation
npm install rest-client-sdk
Usage
Declare your mapping
import { Mapping, Attribute, Relation, ClassMetadata } from 'rest-client-sdk';
const mapping = new Mapping('/v1');
const productMetadata = new ClassMetadata(
'products',
'my_products',
SomeRepositoryClass
);
const idAttr = new Attribute(
'@id',
'id',
'string',
true
);
const name = new Attribute('name');
productMetadata.setAttributeList([idAttr, name]);
productMetadata.setRelationList([
new Relation(
Relation.ONE_TO_MANY,
'categories',
'category_list',
'categoryList'
),
]);
const categoryMetadata = new ClassMetadata('categories');
categoryMetadata.setAttributeList([
new Attribute('id', 'id', 'string', true),
new Attribute('name'),
]);
categoryMetadata.setRelationList([
new Relation(Relation.MANY_TO_ONE, 'product', 'product'),
]);
mapping.setMapping([productMetadata, categoryMetadata]);
Using TypeScript ? You need to configure the mapping to benefits from TypeScript types detection.
type Product = {
@id: string;
name: string;
categoryList: Category[];
};
type Category = {
@id: string;
name: string;
product: Product;
};
type TSMetadata = {
products: {
entity: Product;
list: Array<Product>;
};
categories: {
entity: Category;
list: Array<Category>;
};
};
Create the SDK
Create the token storage
import { TokenStorage } from 'rest-client-sdk';
const tokenGeneratorConfig = { path: 'oauth.me', foo: 'bar' };
const tokenGenerator = new SomeTokenGenerator(tokenGeneratorConfig);
const storage = AsyncStorage;
const tokenStorage = new TokenStorage(tokenGenerator, storage);
The token generator is a class implementing generateToken
and refreshToken
.
Those methods must return an array containing an access_token
key.
The storage needs to be a class implementing setItem(key, value)
,
getItem(key)
and removeItem(key)
. Those functions must return a promise.
At Mapado we use localforage in a
browser environment and
React Native AsyncStorage
for React Native.
Configure the SDK
import RestClientSdk from 'rest-client-sdk';
const config = {
path: 'api.me',
scheme: 'https',
port: 443,
segment: '/my-api',
authorizationType: 'Bearer',
useDefaultParameters: true,
unitOfWorkEnabled: true,
onRefreshTokenFailure: () => {
},
};
const sdk = new RestClientSdk(tokenStorage, config, mapping);
Using TypeScript ? You should now pass the TSMetadata that you defined.
import RestClientSdk, { Token } from 'rest-client-sdk';
const sdk = new RestClientSdk<TSMetadata>(tokenStorage, config, mapping);
UnitOfWork
Adding the key unitOfWorkEnabled
to the config
object passed to the RestClientSdk
constructor will enable or disable the UnitOfWork
The UnitOfWork keeps track of the changes made to entity props so as to only send dirty fields when updating that entity
const productRepo = sdk.getRepository('products');
let product = await productRepo.find(1);
product = product.set('name', 'Huckleberry Finn');
productRepo.update(product);
When dealing with large collections of objects, the UnitOfWork can add a considerable memory overhead. If you do not plan to do updates, it is advised to leave it disabled
Deactivating the UnitOfWork for some calls
You can deactivate unit of work for some calls to avoid registering objects you don't want:
const productRepo = sdk.getRepository('products');
productRepo.find(1, { fields: ALL_PROPERTIES });
productRepo.withUnitOfWork(false).find(1, { fields: ONLY_ID });
The withUnitOfWork
can only be used for find* calls. See #94 for more informations.
You can not activate the unit of work if it has not been enabled globally.
Make calls
Find
You can now call the clients this way:
sdk.getRepository('products').find(8);
sdk.getRepository('products').findAll();
sdk.getRepository('products').findBy({ foo: 'bar' });
All these methods returns promises.
find
returns a Promise<Entity>
, findBy
and findAll
returns Promise<Iterable<Entity>>
Update / delete
sdk.getRepository('products').create(entity);
sdk.getRepository('products').update(entity);
sdk.getRepository('products').delete(entity);
All these methods returns promises.
create
and update
returns a Promise<Entity>
with the new entity.
delete
returns Promise<void>
.
Overriding repository
You can override the default repository
import { AbstractClient } from 'rest-client-sdk';
class SomeEntityClient extends AbstractClient {
getPathBase(pathParameters) {
return '/v2/some_entities';
}
getEntityURI(entity) {
return `${this.getPathBase()}/${entity.id}`;
}
}
export default SomeEntityClient;
Typescript users:
import { SdkMetadata } from 'rest-client-sdk';
class SomeEntityClient extends AbstractClient<TSMetadata['some_entities']> {
getPathBase(pathParameters: object): string {
return '/v2/some_entities';
}
getEntityURI(entity: SomeEntity): string {
return `${this.getPathBase()}/${entity.id}`;
}
findThisPost(params): Post {
}
}
TODO : For the moment, if you want to call a custom repository method, you have to cast it. (TODO : Find a way to get it from the mapping).
const repo = sdk.getRepository('posts') as PostRepository<TSMetadata, Token>;
repo.findThisPost();
Custom serializer
The serializer is the object in charge of converting strings to object and vice-versa.
It deserializes strings from the API in two phases (for both items and lists) :
- converts a string to a plain object (decode)
- optionnally converts this object to a model object (denormalize), if you want to work with something different than plain JS object (like immutable Record or a custom model class).
and on the other way serializes in two phase two:
- optionnaly converts the model object to a plain JavaScript object (normalize)
- converts this plain JavaScript object to a string that will be sent to the API (encode)
It has been greatly inspired by PHP Symfony's serializer.
Default implementation
The default serializer implementations deserializes JSON to plain JavaScript object and serializes plain JavaScript object to JSON.
Creating custom serializer implementation
You can create and inject a custom serializer to the SDK. The serializer must extends the base Serializer
class and implement the following methods:
normalizeItem(entity: object, classMetadata: ClassMetadata): object
: convert an entity to a plain javascript objectencodeItem(object: object, classMetadata: ClassMetadata): string
: convert a plain javascript object to stringdecodeItem(rawData: string, classMetadata: ClassMetadata, response: Response): object
: convert a string containing an object to a plain javascript objectdenormalizeItem(object: object, classMetadata: ClassMetadata, response: Response): object
: convert a plain object to an entitydecodeList(rawListData: string, classMetadata: ClassMetadata, response: Response): object | object[]
: convert a string containing a list of objects to a list of plain javascript objectsdenormalizeList(objectList: object | object[], classMetadata: ClassMetadata, response: Response): object[]
: convert a plain object list to an entity list
classMetadata
is the instance of ClassMetadata you configured. response
is the HTTP response object.
All text response from GET / PUT / POST request will be send to decodeItem + denormalizeItem
or decodeList + denormalizeList
. All content fom update
and create
call will be send to encodeItem + normalizeItem
.
Example with the default serializer
import { Serializer } from 'rest-client-sdk';
class JsSerializer extends Serializer {
normalizeItem(entity, classMetadata) {
return entity;
}
encodeItem(object, classMetadata) {
return JSON.stringify(object);
}
decodeItem(rawData, classMetadata, response) {
return JSON.parse(rawData);
}
denormalizeItem(object, classMetadata, response) {
return object;
}
decodeList(rawListData, classMetadata, response) {
return JSON.parse(rawListData);
}
denormalizeList(objectList, classMetadata, response) {
return objectList;
}
}
const serializer = new JsSerializer();
const sdk = new RestClientSdk(tokenStorage, config, clients, serializer);
Typescript users:
import { Serializer, ClassMetadata } from 'rest-client-sdk';
class JsSerializer extends Serializer {
normalizeItem(entity: object, classMetadata: ClassMetadata): object {
return entity;
}
encodeItem(object: object, classMetadata: ClassMetadata): string {
return JSON.stringify(object);
}
decodeItem(
rawData: string,
classMetadata: ClassMetadata,
response: Response
): object {
return JSON.parse(rawData);
}
denormalizeItem(
object: object,
classMetadata: ClassMetadata,
response: Response
): object {
return object;
}
decodeList(
rawListData: string,
classMetadata: ClassMetadata,
response: Response
): object | object[] {
return JSON.parse(rawListData);
}
denormalizeList(
objectList: object | object[],
classMetadata: ClassMetadata,
response: Response
): object | object[] {
return objectList;
}
}
const serializer = new JsSerializer();
const sdk = new RestClientSdk<TSMetadata>(
tokenStorage,
config,
clients,
serializer
);
Troubleshooting
This sdk uses Object.fromEntries function. We discovered that it is not always available (very old browsers or even in some versions of the IOS JS engine when using the lib with react-native). In this case you need to add a polyfill to the root of your project (or at least before any call is made by the sdk) like so:
function fromEntries(arr) {
return arr.reduce((acc, curr) => {
acc[curr[0]] = curr[1];
return acc;
}, {});
}
Object.fromEntries = Object.fromEntries || fromEntries;