This is a set of classes that help to organize communication between frontend and backend.
These classes are used across all the other libraries within the @sdflc
scope unless it is specified otherwise.
- OpResult - this class represents an operation result that is sent by an API or between modules.
-
ApiWrapper - this class wraps
axios.request
method to send requests to a server and also wraps a response from server intoOpResult
class. -
ApiDataList - this class used to simplify fetching paginated lists of objects from the server. It expects the server to send data as
OpResult
.
This class is used to send data and errors in a unified way between API and frontend or between modules . The object structure looks like this:
{
code: 0, // Result code. Zero is OK, negative value is an error, positive numbers can represent progress
data: [], // Data from the server are always wrapped by an array. Event for one items that server sends it gets wrapped into an array
errors: [] // An array objects describing errors if any. See description below
}
Here is an example of the object with some data:
{
code: 0,
data: [
{
name: 'John Smith',
email: 'jsmith@email.com'
}
],
errors: []
}
Here is an example of the object error after trying to save data:
{
code: 0,
data: [],
errors: [
{
name: '',
errors: [
'Failed to save user information due to lack of access rights.'
],
warnings: [
'Some wraning message'
]
}
]
}
or
{
code: 0,
data: [
{
name: 'John Smith',
email: 'jsmith-email.com'
}
],
errors: [
{
name: 'email',
errors: [
'Email field should should be a valid email address'
]
}
]
}
or
{
code: 0,
data: [
{
name: 'John Smith',
contats: {
email: 'jsmith@email.com',
phone: '4037654321'
}
},
{
name: 'Tom',
contats: {
email: 'tom-email.com',
phone: '4031234567'
}
}
],
errors: {
'users[1].contats.phone': {
errors: [
'Email field should should be a valid email address'
]
}
}
}
The code
property provides information of an operation result and it represents:
- If it is 0 then the operation was successful.
- In case of negative value it means that there was an error and error(s) should be available in the
errors
property. - The value can be positive which mean that the operation is still in progress.
All available error codes are located in the OP_RESULT_CODES
object.
The data
property contains actual data server sends in the response to a request. You must use setData
method to set
your data to the OpResult object.
It is important to keep in mind that data should always be an array. If there is no data then the array should be empty.
Also, data is considered empty when there is one item in the array and it is either null
or undefined
.
The errors
property is an array of objects with information about errors occured when processing a request.
The structure of objects in the errors
array looks like this:
[
{
name: '',
errors: ['Summary error description'],
warnings: [],
},
{
name: fieldName,
errors: ['fieldName error description', 'You can add several errors for the fieldName'],
warnings: [],
},
{
name: otherName,
errors: ['otherName error description', 'You can add several errors for the otherName'],
warnings: [],
},
];
Contructor accepts props that are expected to look like:
{
code: 0,
data: [],
errors: []
}
// The servers returns results in the OpResult ready format
const requestServer = async (props) => {
let result = null;
try {
const response = await axios.get(url);
// The response.data looks like
// { code: CODE, data: [...], errors: [...] }
result = new OpResult(response.data);
} catch (ex) {
// Process exceptions and also add API or network errors to the OpResult...
result = OpResult.fail(OP_RESULT_CODES.EXCEPTION, null, 'Here you can write message that user is supposed to see');
}
return result;
};
The function above does not throw any exceptions. To check if there was any error (from the server or caused by axios)
it is enough to call result.didFail()
and then get error description for example like result.getErrorSummary()
.
Alternatively, if you prefer work with exceptions you could do like this so works with errors is the same no matter how an error occured.
// The servers returns results in the OpResult ready format
const requestServer = async (props) => {
let result = null;
try {
const response = await axios.get(url);
// The response.data looks like
// { code: CODE, data: [...], errors: [...] }
result = new OpResult(response.data);
} catch (ex) {
// Process exceptions and also add API or network errors to the OpResult...
throw OpResult.fail(OP_RESULT_CODES.EXCEPTION, null, 'Here you can write message that user is supposed to see');
}
return result;
};
Sets data to the OpResult
class object:
const r = new OpResult();
r.setData({
name: 'John',
});
Or
const r = new OpResult();
r.setData([
{
name: 'John',
},
]);
Gets data from the OpResult
class object:
const r = new OpResult();
r.setData({
name: 'John',
});
const d = r.getData();
// the `d` will be:
// [
// {
// name: 'John'
// }
// ]
Gets data's first item and if there is no data then it returns defaultValue
:
const r = new OpResult();
r.setData({
name: 'John',
});
const d = r.getDataFirst();
// the `d` will be:
// {
// name: 'John'
// }
Sets code to the OpResult
class object:
import { OP_RESULT_CODES } from '@sdflc/api-helpers';
const r = new OpResult();
r.setCode(OP_RESULT_CODES.FAILED);
Gets code from the OpResult
class object:
import { OP_RESULT_CODES } from '@sdflc/api-helpers';
const r = new OpResult();
r.setCode(OP_RESULT_CODES.FAILED);
const code = r.getCode(); // => code = -10000
Adds an error message to specified field errors.
Here is a simple example of a server side function that accepts formData
, does some checks
and in case of wrong data adds errors and then returns OpResult object.
import { OP_RESULT_CODES } from '@sdflc/api-helpers';
const save = async (formData) => {
const r = new OpResult();
if (!formData) {
return r.addError('', 'No form data provided', OP_RESULT_CODES.VALIDATION_FAILED);
}
if (!formData.name) {
r.addError('name', 'You must provide user name', OP_RESULT_CODES.VALIDATION_FAILED);
}
if (!checkEmail(formData.email)) {
r.addError('email', 'Email field must be a valid email address', OP_RESULT_CODES.VALIDATION_FAILED);
}
if (r.hasErrors()) {
return r;
}
try {
saveUser(formData);
} catch (ex) {
r.addError('', 'Oops, something went wrong when saving data. Try again', OP_RESULT_CODES.EXCEPTION);
}
return r;
};
Returns true if there is at least one error in the errors
property.
import { OP_RESULT_CODES } from '@sdflc/api-helpers';
const r = new OpResult();
r.addError('', 'Oops, something went wrong when saving data. Try again', OP_RESULT_CODES.EXCEPTION);
if (r.hasErrors()) {
console.log('We have errors');
}
Clears all added errors.
import { OP_RESULT_CODES } from '@sdflc/api-helpers';
const r = new OpResult();
r.addError('', 'Oops, something went wrong when saving data. Try again', OP_RESULT_CODES.EXCEPTION);
r.clearErrors(); // => errors: {}
Used to apply passed class to all OpResult's data items, ie. convert from anonymous data items to specfic ones. Here is a simple example:
import { OP_RESULT_CODES } from '@sdflc/api-helpers';
class User {
name: string = '';
email: string = '';
constructor(props) {
if (!props) {
props = {};
}
this.name = props.name || '';
this.email = props.email || '';
}
}
// or using vanilla JavaScript
// function User(props) {
// if (!props) {
// props = {}
// }
// this.name = props.name || '';
// this.email = props.email || '';
// }
const getRq = async (userData) => {
const response = await axios.get(url);
const result = new OpResult(response.data);
// by now result.data = [{ name: 'John', email: 'john@gmail.com' }]
result.applyModelClass(User);
// by now result.data = [User { name: 'John', email: 'john@gmail.com' }]
};
Returns true if the code
isequal to OP_RESULT_CODES.OK.
Returns true if the code
is not equal to OP_RESULT_CODES.OK.
Returns true if the data
has at least one element in the array and it is not equal to null or undefined.
const r = new OpResult();
r.setData({ name: 'John' });
r.hasData(); // => true as data = [{ name: 'John' }]
r.setData(null);
r.hasData(); // => false as data = [null]
r.setData([null, { name: 'John' }]);
r.hasData(); // => true as data = [null, { name: 'John' }]
Returns true if there are elements in the OpResult.errors array
Returns true if the code
is equal to OP_RESULT_CODES.OK and hasData() === true
.
Returns true if the code
is equal to OP_RESULT_CODES.LOADING. The method usually used by frontend to track status of get
request.
Returns true if the code
is equal to OP_RESULT_CODES.SAVING. The method usually used by frontend to track status of post/put
request.
Returns true if the code
is equal to OP_RESULT_CODES.DELETING. The method usually used by frontend to track status of delete
request.
Returns true if the code
is equal to either OP_RESULT_CODES.LOADING, OP_RESULT_CODES.SAVING or OP_RESULT_CODES.DELETING.
The method is usually used by frontend to track status of a request.
Sets code
to OP_RESULT_CODES.LOADING. The method is usually used by frontend to track status of get
request.
Sets code
to OP_RESULT_CODES.SAVING. The method is usually used by frontend to track status of post/put
request.
Sets code
to OP_RESULT_CODES.DELETING. The method is usually used by frontend to track status of delete
request.
Create a copy of the OpResult
object.
const r = new OpResult();
r.setData({ name: 'John' });
r.setCode(OP_RESULT_CODES.FAILED);
const r2 = r.clone();
// r2 is a new object with data = [{ name: 'John' }] and code = OP_RESULT_CODES.FAILED
Note that no deep data copy happens in this case.
Returns errors summary in one string object for the specified field.
Generic erros are usually passed in the ''
field.
import { OP_RESULT_CODES } from '@sdflc/api-helpers';
const r = new OpResult();
r.addError('', 'Error 1.');
r.addError('', 'Error 2.', OP_RESULT_CODES.EXCEPTION);
r.getErrorSummary(''); // => 'Error 1. Error 2.'
Returns array with all errors
keys.
import { OP_RESULT_CODES } from '@sdflc/api-helpers';
const r = new OpResult();
r.addError('name', 'Wrong name', OP_RESULT_CODES.VALIDATION_FAILED));
r.addError('email', 'Invalid email address.', OP_RESULT_CODES.VALIDATION_FAILED);
r.getErrorFields(); // => ['name', 'email']
Returns array with all errors for the specified field:
import { OP_RESULT_CODES } from '@sdflc/api-helpers';
const r = new OpResult();
r.addError('', 'Error 1.', OP_RESULT_CODES.EXCEPTION);
r.addError('', 'Error 2.', OP_RESULT_CODES.EXCEPTION);
r.getFieldErrors(''); // => ['Error 1', 'Error 2']
It is suppsed to be used when data
property has just one element in it.
The method takes first element from the data
property, and then tries to get a value fieldName
.
If the value is null
or undefined
then it returns defaultValue
. If the fieldName
is a function
it calls the function and returns its result.
const r = new OpResult();
r.setData({
firstName: 'John',
lastName: 'Smith',
fullName: (obj) => {
return `${obj.firstName} ${obj.lastName}`;
},
});
r.getDataFieldValue('fullName'); // => John Smith
Returns an object containg properties code
, data
, errors
. It is used to send data back to the frontend:
const r = new OpResult();
r.setData({
firstName: 'John',
lastName: 'Smith',
});
r.toJS(); // { code: 0, data: [{ firstName: 'John', lastName: 'Smith' }], errors: {} }
Returns stringified result of toJS()
.
Returns HTTP Status Code depending on value in the code
property.
For example,
- if code = OP_RESULT_CODES.EXCEPTION then the function will return 500.
- if code = OP_RESULT_CODES.NOT_FOUND then the function will return 404.
This is a static function to simplify creating of OpResult object with data:
const r = OpResult.ok({
firstName: 'John',
lastName: 'Smith',
});
This is a static function to simplify creating of OpResult object with simple error information:
const r = OpResult.fail(OP_RESULT_CODES.NOT_FOUND, {}, 'Object not found');
The helper class wraps axios.request
method to do a request to the server and then pass received JSON object
into the OpResult
for further work. Also, the class catches all exceptions that may happen and also returns OpResult object.
The property baseApiUrl
stores root path to the API. For example, 'https://my-api.com/v1/'.
Note that it must end with '/'.
The onException
property is a function that is called if some exception happens. This is per request property.
The fetchFnOpts
defines default configuration parameters supplied to axios.request
method. By default it looks like:
//...
static fetchFnOpts: any = {
withCredentials: true,
timeout: 0,
};
//...
This is static function used by all instances of the ApiWrapper
and it does actuall call of the axios.request
.
You can override the function if you want to use another library to send requests. Just make sure it returns response
the same way as axios.request
.
This is the function that is assigned to each ApiWrapper
instance if no OnException
prop passed to constructor.
By default, the function just does console.error
with the information about an exception.
Sends GET request to the server with provided path and params.
const api = new ApiWrapper({ baseApiUrl: 'https://my-server.com/v1/' });
const r = await api.get('user/123', { some: 'something' }); // => GET https://my-server.com/v1/user/123?some=something
// r = {
// code: 0,
// data: [
// {
// name: 'John'
// }
// ],
// errors: {}
// }
// or
// r = {
// code: -20200,
// data: [],
// errors: {
// name: {
// errors: ['Such user not found']
// }
// }
// }
Sends POST request to the server with provided path and params. Used to create an entity on the server.
const api = new ApiWrapper({ baseApiUrl: 'https://my-server.com/v1/' });
const r = await api.post('user', { name: 'John' }); // => POST https://my-server.com/v1/user
// r = {
// code: 0,
// data: [
// {
// id: 123,
// name: 'John'
// }
// ],
// errors: {}
// }
// or
// r = {
// code: -20300,
// data: [],
// errors: {
// name: {
// errors: ['Such user already exists']
// }
// }
// }
Sends POST request to the server with provided path and params. Used to create an entity on the server.
const api = new ApiWrapper({ baseApiUrl: 'https://my-server.com/v1/' });
const r = await api.put('user/123', { name: 'Tom' }); // => PUT https://my-server.com/v1/user/123
// r = {
// code: 0,
// data: [
// {
// id: 123,
// name: 'Tom'
// }
// ],
// errors: {}
// }
// or
// r = {
// code: -20300,
// data: [],
// errors: {
// name: {
// errors: ['Such user already exists']
// }
// }
// }
Sends DELETE request to the server with provided path and params. Used to create an entity on the server.
const api = new ApiWrapper({ baseApiUrl: 'https://my-server.com/v1/' });
const r = await api.delete('user/123'); // => DELETE https://my-server.com/v1/user/123
// r = {
// code: 0,
// data: [],
// errors: {}
// }
// or
// r = {
// code: -20200,
// data: [],
// errors: {
// name: {
// errors: ['Cannot delete the user as it is not found']
// }
// }
// }
The helper class helps to simplify fetching paginated lists of objects from the server providing the server sends data
using the OpResult
structure. The class uses both ApiWrapper
and OpResult
in its operation. Fetched pages
are cached in the memory.
Constructor of the class expects the following properties to be passed:
- baseApiUrl - mandatory - base API URL, example: 'https://app.com/api/v1' or 'https://app.com/api/v1/users'.
-
mode - optional - specifies what to do with page number each time
fetchList
method is used. Default value is to increase page number by one on each call. - modelClass - optional - specifies an object to use for wrapping each item of received list. The class should accept raw object in its constructor to inialize its props.
- params - optional - is an object that will be passed to the server as URL query params.
-
transform - optional - is a function used to transform each object of received list before applying
modelClass
if any.
Used to clone the object including arrays with received data. New arrays with data reference the same objects though.
Used to parse orderBy
parameter from a string to an object. The string should have pattern like this field1-(asc|desc)~field2-(asc|desc)
.
For example, for the string name-asc~orderDate-desc
will be converted into the object
{
name: 'asc',
orderDate: 'desc'
}
Clears the class instance state.
Used to set new base API URL for the instance.
Used to set new modelClass
class. By setting new modelClass
you reset current state so you need to refetch data.
Sets new fetch mode. Supported modes are:
-
STAY (
API_DATALIST_FETCH_MODES.STAY
) - stay on the same page each timefetchList
is called; -
FORWARD (
API_DATALIST_FETCH_MODES.FORWARD
) - increase page number each timefetchList
is called; -
BACK (
API_DATALIST_FETCH_MODES.BAKC
) - decrease page number each timefetchList
is called;
Sets query parameters to uses when fetching data. The params
is an object that will be transformed into URL query string.
If reset = true
then resets object's inner state and clears all already loaded data. Example:
const dataList = new ApiDataList({ ... });
...
const params = {
projectId: '123',
label: 'lbl'
}
dataList.setParams(params);
dataList.fetchList() // https://baseurlapi/path?projectId=123&label=lbl
Append new parameters or replace existing parameters. If reset = true
then resets object's inner state and
clears all already loaded data. Example:
const existingParams = dataList.getParams();
// existingParams = {
// projectId: '123',
// label: 'lbl'
// };
dataList.appendParams({
projectId: '456',
status: 'open',
});
// dataList.getParams() = {
// projectId: '456',
// label: 'lbl',
// status: 'open'
// };
Append new parameters or replace existing parameters. If reset = true
then resets object's inner state and
clears all already loaded data. Example:
const removeParams = dataList.getParams();
// existingParams = {
// projectId: '123',
// label: 'lbl',
// status: 'open'
// };
dataList.removeParams(['label', 'status']);
// dataList.getParams() = {
// projectId: '456'
// };
Returns existing params.
Returns existing params object.
Sets new page size. If reset = true
then resets object's inner state and clears all already loaded data.
Sets new orderBy property. The orderBy
can be either an object or string in a specified format. Examples:
dataList.setOrderBy({ name: 'asc', dateOrder: 'desc' }); // should be used on the frontend side
dataList.setOrderBy('name-asc~dateOrder-desc'); // should be used on the back-end side to initialize ApiDataList object with orerBy property
If reset = true
then resets object's inner state and clears all already loaded data.
Toggles (asc/desc) orderBy
property for provided field. If no field provided it toggles all fields in orderBy
.
If reset = true
then resets object's inner state and clears all already loaded data.
Sets new page number. If page less than zero sets it as zero.
Increases page number by one.
Decreases page number by one.
Returns curent page number.
Returns true if the mode is FORWARD
and it is first call or previously loaded list items length equals to pageSize
or the mode is BACK
and current page is greater than 1.
Does call to the server API to fetch data list. The path
is optional and if present then it is added
to the baseApiUrl
property. If there is no error the data list gets added to inner state pages
object.
The method returns OpResult
object so user can get access to possible error details.
Returns pages count requested by this moment.
Returns items for specified page or for current page.
Returns items for all pages requested by this moment.
Sets loading state to the inner state OpResult object. This may be used to change UI accordingly to let a user know that list is being loaded.
Returns true if the request is still in progress.
Returns true if the request succeeded.
Returns true if the request failed.
Returns request result as OpResult
object.
Returns number of items to skip when doing query to the data source. It should used on the server side and is calculated as (page - 1) * pageSize.
Returns page size used to query this amount of rows from the data source. It should be used on the server side.
Returns param's orderBy
object.
The QueryGraphQLArgs
has the following paramters:
-
url: string
- a required URL of the GraphQL server -
queryName: string
- a required name of query in the query string, used to extract result from response -
query: string
- a required query string to be sent -
variables?: any
- an object representing variables to send along with the query -
headers?: any
- an object with HTTP headers, for example authorization header
Example of usage:
const result = await queryGraphQL({
url: 'http://localhost:4000',
query: `
query SignIn($params: SignInInput) {
signIn(params: $params) {
code
errors {
name
errors
warnings
}
data {
id
username
email
firstName
middleName
lastName
}
}
}
`,
variables: {
username: 'testuser',
password: 'somepassword',
},
headers: {
'x-api-key': 'some-api-key',
},
});
// result.data =>
// {
// code: 0,
// errors: [],
// data: {
// id: 1,
// username: 'testuser',
// email: 'some@gmail.com',
// firstName: 'Test',
// middleName: '',
// lastName: 'User',
// }
// }