@sdflc/api-helpers
TypeScript icon, indicating that this package has built-in type declarations

1.1.11 • Public • Published

@sdflc/api-helpers

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.

Classes overview

  • 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 into OpResult class.
  • ApiDataList - this class used to simplify fetching paginated lists of objects from the server. It expects the server to send data as OpResult.

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

OpResult properties

code

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.

data

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.

errors

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: [],
  },
];

OpResult methods

constructor(props)

Contructor accepts props that are expected to look like:

{
  code: 0,
  data: [],
  errors: []
}
Example
// 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;
};

setData(data: any)

Sets data to the OpResult class object:

const r = new OpResult();

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

Or

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

setCode(code: number)

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);

getCode()

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

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

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

hasErrors()

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

clearErrors()

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: {}

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

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 frontend to track status of get request.

isSaving()

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.

isDeleting()

Returns true if the code is equal to OP_RESULT_CODES.DELETING. The method usually used by frontend 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 is usually used by frontend to track status of a request.

startLoading()

Sets code to OP_RESULT_CODES.LOADING. The method is usually used by frontend to track status of get request.

startSaving()

Sets code to OP_RESULT_CODES.SAVING. The method is usually used by frontend to track status of post/put request.

startDeleting()

Sets code to OP_RESULT_CODES.DELETING. The method is usually used by frontend 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

Note that no deep data copy happens in this case.

getErrorSummary(field?: string)

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

getErrorFields()

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

getFieldErrors(field: string)

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

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 frontend:

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 a 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 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');

ApiWrapper

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.

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 does console.error with the information about an 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 the 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 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.

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.

GraphQL Helpers

queryGraphQL(args: QueryGraphQLArgs): OpResult

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

Readme

Keywords

Package Sidebar

Install

npm i @sdflc/api-helpers

Weekly Downloads

5

Version

1.1.11

License

ISC

Unpacked Size

101 kB

Total Files

22

Last publish

Collaborators

  • dcerge