@sdflc/api-helpers

A set of classes that help to send api calls from front end and then then back from back end.

Usage no npm install needed!

<script type="module">
  import sdflcApiHelpers from 'https://cdn.skypack.dev/@sdflc/api-helpers';
</script>

README

@sdflc/api-helpers

This is a set of classes that help to organize communication between front end and server. These classes are used across all the other libraries within @sdflc scope unless it is specified otherwise.

Classes overview

  • OpResult - this class represents an operation result that is expected by a front-end app and that should be sent by a server.
  • ApiWrapper - this class wraps axios.request method to do requests to a server and also wraps a response from server into OpResult class.
  • ApiDataList - this class used to simplify fetching paginated lists of objects from the server. It expects the server sends data as OpResult structure.

OpResult

This class is used to send data from server to the front-end in a unified way as well as wrap received JSON object from server in this class on the front-end side. This helps to works with data the same way as on the server as well as on the front-end. The object structure basically looks like this:

{
  code: 0,   // Result code. Zero is OK, negative value is an error
  data: [],  // Data from server are always wrapped by an array. Event for one items that server sends it gets wrapped into an array
  errors: [] // An array with 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.'
      ]
    }
  ]
}

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'
      ]
    }
  }
}

OpResults properties

code

The code property provides information of an operation result. If it is 0 then you the operation was successful. In case of negative value it means that there was an error and the error details should be available in the errors property. Your code should set corresponding errors, of course. The value can be positive which mean that the operation is still in progress.

All available code values are located in the OP_RESULT_CODE object.

data

The data property contains actual data server sends to back to front-end or other side. You must use setData method to set your data to the OpResult object. It is important to keep in mind that data is alwat an array. If there is no data then the array is empty. Also, data is considered empty when there is one item in the array and it is either null or undefined.

errors

The errors property is an array that contains information about errors occured during executing an operation. The structure of the errors object has the following structure:

[
  {
    name: '',
    errors: ['Summary error description'],
  },
  {
    name: fieldName,
    errors: ['fieldName error description', 'You can add several errors for the fieldName'],
  },
  {
    name: otherName,
    errors: ['otherName error description', 'You can add several errors for the otherName'],
  },
];

OpResults methods

constructor(props)

Contructor accepts props that are expected to look like:

{
  code: 0,
  data: [],
  errors: []
}

Setting the class properties via constructor works best when you receive result object from server and need to initialize OpResult accordingly.

Example

const requestServer = async (props) => {
  const response = await axios.get(url);
  const result = new OpResult(response.data);
};

setData(data: any)

Sets data to the OpResult class object:

const r = new OpResult();

r.setData({
  name: 'John',
});

getData()

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'
//   }
// ]

getDataFirst(defaultValue: any)

Gets data's first item and 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'
// }

setCode(code: number)

Sets code to the OpResult class object:

import { OP_RESULT_CODE } from '@sdflc/api-helpers';

const r = new OpResult();

r.setCode(OP_RESULT_CODE.FAILED);

getCode()

Gets code from the OpResult class object:

import { OP_RESULT_CODE } from '@sdflc/api-helpers';

const r = new OpResult();

r.setCode(OP_RESULT_CODE.FAILED);

const code = r.getCode(); // => code = -10000

addError(field: string, errorMessage: string, code?: number)

Adds an error message to specified field errors. Here is an simple example of a server side function that accepts formData, does some checks and in case of wrong data add errors adn return OpResult object.

import { OP_RESULT_CODE } from '@sdflc/api-helpers';

const save = async (formData) => {
  const r = new OpResult();

  if (!formData) {
    return r.addError('', 'No form data provided', OP_RESULT_CODE.VALIDATION_FAILED);
  }

  if (!formData.name) {
    r.addError('name', 'You must provide user name', OP_RESULT_CODE.VALIDATION_FAILED);
  }

  if (!checkEmail(formData.email)) {
    r.addError('email', 'Email field must be a valid email address', OP_RESULT_CODE.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_CODE.EXCEPTION);
  }

  return r;
};

hasErrors()

Returns true if there is at least one error in the errors property.

import { OP_RESULT_CODE } from '@sdflc/api-helpers';

const r = new OpResult();

r.addError('', 'Oops, something went wrong when saving data. Try again', OP_RESULT_CODE.EXCEPTION);
if (r.hasErrors()) {
  console.log('We have errors');
}

clearErrors()

Clears all added errors.

import { OP_RESULT_CODE } from '@sdflc/api-helpers';

const r = new OpResult();

r.addError('', 'Oops, something went wrong when saving data. Try again', OP_RESULT_CODE.EXCEPTION);
r.clearErrors(); // => errors: {}

applyModelClass(modelClass: any)

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_CODE } 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' }]
};

didSucceed()

Returns true if the code isequal to OP_RESULT_CODES.OK.

didFail()

Returns true if the code is not equal to OP_RESULT_CODES.OK.

hasData()

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' }]

hasErrors()

Returns true if there are elements in the OpResult.errors array

didSucceedAndHasData()

Returns true if the code is equal to OP_RESULT_CODES.OK and hasData() === true.

isLoading()

Returns true if the code is equal to OP_RESULT_CODES.LOADING. The method usually used by front-end to track status of get request.

isSaving()

Returns true if the code is equal to OP_RESULT_CODES.SAVING. The method usually used by front-end to track status of post/put request.

isDeleting()

Returns true if the code is equal to OP_RESULT_CODES.DELETING. The method usually used by front-end to track status of delete request.

isInProgress()

Returns true if the code is equal to either OP_RESULT_CODES.LOADING, OP_RESULT_CODES.SAVING or OP_RESULT_CODES.DELETING. The method usually used by front-end to track status of a request.

startLoading()

Sets code to OP_RESULT_CODES.LOADING. The method usually used by front-end to track status of get request.

startSaving()

Sets code to OP_RESULT_CODES.SAVING. The method usually used by front-end to track status of post/put request.

startDeleting()

Sets code to OP_RESULT_CODES.DELETING. The method usually used by front-end to track status of delete request.

clone()

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

getErrorSummary(field?: string)

Returns errors summary in one string object for the specified field.

import { OP_RESULT_CODE } from '@sdflc/api-helpers';

const r = new OpResult();

r.addError('', 'Error 1.', OP_RESULT_CODE.EXCEPTION);
r.addError('', 'Error 2.', OP_RESULT_CODE.EXCEPTION);
r.getErrorSummary(''); // => 'Error 1. Error 2.'

getErrorFields()

Returns array with all errors keys.

import { OP_RESULT_CODE } from '@sdflc/api-helpers';

const r = new OpResult();

r.addError('name', 'Wrong name', OP_RESULT_CODE.VALIDATION_FAILED));
r.addError('email', 'Invalid email address.', OP_RESULT_CODE.VALIDATION_FAILED);
r.getErrorFields(); // => ['name', 'email']

getFieldErrors(field: string)

Returns array with all errors for the specified field:

import { OP_RESULT_CODE } from '@sdflc/api-helpers';

const r = new OpResult();

r.addError('', 'Error 1.', OP_RESULT_CODE.EXCEPTION);
r.addError('', 'Error 2.', OP_RESULT_CODE.EXCEPTION);
r.getFieldErrors(''); // => ['Error 1', 'Error 2']

getDataFieldValue(fieldName: string, defaultValue: string = '')

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

toJS

Returns an object containg properties code, data, errors. It is used to send data back to the front-end:

const r = new OpResult();

r.setData({
  firstName: 'John',
  lastName: 'Smith',
});
r.toJS(); // { code: 0, data: [{ firstName: 'John', lastName: 'Smith' }], errors: {} }

toJSON()

Returns stringified result of toJS().

getHttpStatus()

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.

static ok(data?: any, opt?: any)

This is static function to simplify creating of OpResult object with data:

const r = OpResult.ok({
  firstName: 'John',
  lastName: 'Smith',
});

static fail(code: number, data: any, message: string, opt?: any)

This is static function to simplify creating of OpResult object with simple error information:

const r = OpResult.fail(OP_RESULT_CODES.NOT_FOUND, {}, 'Object not found');

ApiWrapper

The helper class wraps axios.request method to do a request to the server and then pass received json object into OpResult for further work. Also, the class catches all exceptions that may happen and also returns OpResult object.

ApiWrapper propeties

baseApiUrl

The property baseApiUrl stores root path to the API. For example, 'https://my-api.com/v1/'. Note that it must end with '/'.

onException

The onException property is a function that is called if some exception happens. This is per request property.

static fetchFnOpts

The fetchFnOpts defines default configuration parameters supplied to axios.request method. By default it looks like:

...
static fetchFnOpts: any = {
  withCredentials: true,
  timeout: 0,
};
...

static fetcnFn

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.

static onExceptionFn

This is the function that is assigned to each ApiWrapper instance if no OnException prop passed to constructor. By default, the function just console.error information about exception.

ApiWrapper methods

get(path: string, params: any)

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']
//     }
//   }
// }

post(path: string, data?: any, params: any = {})

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']
//     }
//   }
// }

put(path: string, data?: any, params: any = {})

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']
//     }
//   }
// }

delete(path: string, data: any = {}, params: any = {})

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']
//     }
//   }
// }

ApiDataList

The helper class helps to simplify fetching paginated lists of objects from the server providing the server sends data using OpResult structure. The class uses both ApiWrapper and OpResult in its operation. Fetched pages are cached in the memory.

Constructor and methods

constructor(props: any)

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.

clone()

Used to clone the object including arrays with received data. New arrays with data reference the same objects though.

parseOrderBy(orderBy: string)

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'
}

resetState()

Clears the class instance state.

setBaseUrl(baseApiUrl: string)

Used to set new base API URL for the instance.

setModelClass(modelClass: any)

Used to set new modelClass class. By setting new modelClass you reset current state so you need to refetch data.

setMode(mode: string)

Sets new fetch mode. Supported modes are:

  • STAY (API_DATALIST_FETCH_MODES.STAY) - stay on the same page each time fetchList is called;
  • FORWARD (API_DATALIST_FETCH_MODES.FORWARD) - increase page number each time fetchList is called;
  • BACK (API_DATALIST_FETCH_MODES.BAKC) - decrease page number each time fetchList is called;

setParams(params: any, reset?: boolean)

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

appendParams(params: any, reset?: boolean)

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'
// };

removeParams(keys: string[], reset?: boolean)

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'
// };

resetParams(reset?: boolean)

Returns existing params.

getParams()

Returns existing params object.

setPageSize(pageSize: number, reset?: boolean)

Sets new page size. If reset = true then resets object's inner state and clears all already loaded data.

setOrderBy(orderBy: any, reset?: boolean)

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 front-end 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.

toggleOrderBy(key: string, reset?: boolean)

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.

setPage(page: number)

Sets new page number. If page less than zero sets it as zero.

toNextPage()

Increases page number by one.

toPrevPage()

Decreases page number by one.

getPage()

Returns curent page number.

canFetchMode()

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.

fetchList(path: string = '')

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.

getTotalPages()

Returns pages count requested by this moment.

getPageItems(page: number = -1)

Returns items for specified page or for current page.

getItems()

Returns items for all pages requested by this moment.

startLoading()

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.

isLoading()

Returns true if the request is still in progress.

didSucceed()

Returns true if the request succeeded.

didFail()

Returns true if the request failed.

getResult()

Returns request result as OpResult object.

getSkip()

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.

getPageSize()

Returns page size used to query this amount of rows from the data source. It should be used on the server side.

getOrderBy()

Returns param's orderBy object.