API List
A package to handle Janis List APIs
Installation
npm install @janiscommerce/api-list
Usage
'use strict';
const { ApiListData } = require('@janiscommerce/api-list');
module.exports = class MyApiListData extends ApiListData {
get alwaysCallFormatter() {
return true;
}
get fieldsToSelect() {
return ['name', 'status'];
}
get fieldsToExclude() {
return ['error'];
}
get fixedFields() {
return ['code'];
}
get sortableFields() {
return [
'id',
'status'
];
}
get availableFilters() {
return [
'id',
{
name: 'quantity',
valueMapper: Number
},
{
name: 'hasSubProperty',
internalName: (filterConfiguration, mappedValue, originalValue) => `rootProperty.${originalValue}`,
valueMapper: () => true
}
];
}
get searchFilters() {
return [
'id',
'quantity'
];
}
async formatRows(rows) {
return rows.map(row => ({ ...row, oneMoreField: true }));
}
};
ApiListData
The following getters and methods can be used to customize and validate your List API.
All of them are optional.
get modelName()
Returns model name. It is intent to be used to change the model's name and it will not get the model name from endpoint
get alwaysCallFormatter()
This is used to force calling the formatRows()
method even if fields
or excludeFields
are sent to the API.
get fieldsToSelect()
This is used to determinate which fields should be selected from the DB.
Important: The id
field is always returned.
If set as false
. The parameter fields
will be ignored.
If a field is not found in the document it will be ignored.
get fieldsToExclude()
Since 5.8.0
This is used to determinate witch fields must be excluded from the response.
If set as false
. The parameter excludeFields
will be ignored.
Important: The id
field is always returned.
If a field is not found in the document it will be ignored.
get fixedFields()
Since 5.8.0
This is used to determinate witch fields should always be returned.
If a field is not found in the document it will be ignored.
async formatRows(rows)
You can use this to format your records before they are returned.
For example, mapping DB friendly values to user friendly values, add default values, translation keys, etc.
get sortableFields()
This is used to indicate which fields can be used to sort the list. Any other sort field will return a 400 status code.
Example using sortableFields()
'use strict';
const { ApiListData } = require('@janiscommerce/api-list');
module.exports = class MyApiListData extends ApiListData {
get sortableFields() {
return ['foo', 'bar'];
}
};
/api/entity?sortBy=foo
with a single value.
Will sort the list by foo
in direction asc
that are the default
/api/entity?sortBy=foo&sortDirection=desc
with a single value.
Will sort the list by foo
in direction desc
/api/entity?sortBy[0]=foo&sortBy[1]=bar
with a single value.
Will sort the list by foo
and bar
in direction asc
that are the default
/api/entity?sortBy[0]=foo&sortBy[1]=bar&sortDirection=desc
with a single value.
Will sort the list by foo
and bar
in direction desc
/api/entity?sortBy[0]=foo&sortBy[1]=bar&sortDirection[0]=desc&sortDirection[1]=asc
with a single value.
Will sort the list by foo
in direction desc
and bar
in direction asc
. The sortDirection is indexed with sortBy
/api/entity?sortBy[0]=foo&sortBy[1]=bar&sortDirection[1]=desc
with a single value.
Will sort the list by foo
in direction asc
because is the default value and bar
in direction desc
**Using sortableFields objects with valueMapper**
Use sortable field valueMapper to return sorts for apply in database instead of sortable field name
'use strict';
const { ApiListData } = require('@janiscommerce/api-list');
module.exports = class MyApiListData extends ApiListData {
get sortableFields() {
return [
{
name: 'foo',
valueMapper: () => [['bar0', 'asc'], ['bar1']]
},
{
name: 'bar',
valueMapper: direction => (
direction ? [['bar0', direction], ['bar1', direction]]: [['bar0', 'asc'], ['bar1']]
)
}
];
}
};
get availableFilters()
This is used to indicate which fields can be used to filter the list. Any other filter will return a 400 status code.
Filters can be customized by passing an object with the following properties:
name
: (string) The name of the filter param. This property is required.internalName
: (string|function) The name of the field, as defined in the model. This should not be defined in case it's equal to name
.
If it's a function (since 3.1.0), it must return a string and it will receive the following arguments: (filterConfiguration, mappedValue, originalValue)
valueMapper
: (function) A function to be called on the filter's value. This is optional.
Value mappers
Since 3.1.0
This lib also exports some value mappers (to use as valueMapper
) so you don't need to implement them yourself.
:warning: Warning startOfTheDayMapper
and endOfTheDayMapper
are now deprecated. See migration guide.
They are explained here with examples:
'use strict';
const {
ApiListData,
FilterMappers: {
booleanMapper,
dateMapper,
startOfTheDayMapper,
endOfTheDayMapper,
searchMapper,
customTypeMapper
}
} = require('@janiscommerce/api-list');
module.exports = class MyApiListData extends ApiListData {
get availableFilters() {
return [
{
name: 'canDoSomething',
valueMapper: booleanMapper
},
{
name: 'someExactDate',
valueMapper: dateMapper
},
{
name: 'dateCreatedDay',
internalName: 'dateCreatedFrom',
valueMapper: startOfTheDayMapper
},
{
name: 'dateCreatedDay',
internalName: 'dateCreatedTo',
valueMapper: endOfTheDayMapper
},
{
name: 'name',
valueMapper: searchMapper
},
{
name: 'isOlderThan',
internalName: 'age',
valueMapper: customTypeMapper('greater')
}
];
}
};
get searchFilters()
Since 3.3.0
This is used to indicate which fields will be used to mapped multiple filters (OR Type) for the same value, using only search
as single filter.
If it don't exist or return an empty array and try to use search
filter will return 400 status code.
Can be combined with other filters.
Example using searchFilters()
'use strict';
const {
ApiListData
} = require('@janiscommerce/api-list');
module.exports = class MyApiListData extends ApiListData {
get searchFilters() {
return ['someField', 'otherField'];
}
};
/api/entity?filters[search]=1
with a single value.
Will filter the list for someField: 1
or otherField: 1
/api/entity?filters[search]=fo
with a uncompleted word.
Will filter the list for someField: fo
or otherField: fo
and will do a partial filter (like using searchMapper
).
/api/entity?filters[search]=foo bar
with multiples words separated by white spaces.
Will filter the list for someField: foo
or someField: bar
or otherField: foo
or otherField: bar
.
get staticFilters()
Since 3.4.0
This is used to set a filter with a fixed value for all requests.
Example using staticFilters()
'use strict';
const {
ApiListData
} = require('@janiscommerce/api-list');
module.exports = class MyApiListData extends ApiListData {
get staticFilters() {
return {
someExactDate: new Date('2020-02-27T14:23:44.963Z'),
clients: this.session.clientCode
};
}
};
This will add two filters (someExactDate
and clients
) to the request filters (if any). The static filters will not be overriden by user-provided filters.
async formatFilters(filters)
Since 4.1.0
This is used to programatically alter the filters. It will be executed after parsing static and dynamic filters.
If you return a falsy value it will not override them. Otherwise, the return value will be used as filters.
You can use this method, for example, to build complex filters or to ensure that a Date range filter is always being applied.
Example using formatFilters()
'use strict';
const {
ApiListData
} = require('@janiscommerce/api-list');
module.exports = class MyApiListData extends ApiListData {
async formatFilters(filters) {
if(filters.someDateFilterFrom && filters.someDateFilterFrom < new Date('2020-01-01T00:00:00.000Z')) {
return {
...filters,
someDateFilterFrom: new Date('2020-02-27T14:23:44.963Z')
};
}
}
};
get customParameters()
This allows you to set custom query parameters for your API.
Can be customized by passing a string or object with the following properties:
name
: (string) The name of the custom param. This property is required.valueMapper
: (function) A function to be called on the parameter's value. This is optional.
The customParameters
and its values will be in this.data
to use them in your API.
Example using customParameters()
'use strict';
const { ApiListData } = require('@janiscommerce/api-list');
module.exports = class MyApi extends ApiListData {
get customParameters() {
return [
'someParam',
{
name: 'numericParam',
valueMapper: Number
}
];
}
async formatRows(rows) {
if(this.data.numericParam === 1)
return rows.map(row => ({ ...row, oneMoreField: true }));
return rows;
}
};
async formatSortables(sortables)
Since 5.4.0
This is used to programatically alter the sortables. It will be executed after parsing static and dynamic sortables.
If you return a falsy value it will not override them. Otherwise, the return value will be used as sortables.
You can use this method, for example, to build complex sorting.
Example using formatSortables()
'use strict';
const {
ApiListData
} = require('@janiscommerce/api-list');
module.exports = class MyApiListData extends ApiListData {
async formatSortables(sortables) {
return Object.keys(sortables).reduce((currentSorts, key) => {
if(key === 'customFilter') {
const customSorts = { someField: 'asc', otherField: 'desc' };
return { ...currentSorts, ...customSorts };
}
return { ...currentSorts, [key]: sorts[key] };
}, {});
}
};
get maxPageSize()
Since 5.5.0
This getter allow to configure a different maximum page-size than default: 100.
Example using maxPageSize()
'use strict';
const {
ApiListData
} = require('@janiscommerce/api-list');
module.exports = class MyApiListData extends ApiListData {
get maxPageSize() {
return 500;
}
};
Reducing responses
Since 5.8.0
An Api defined with ApiListData can be reduced using new parameters fields
and excludeFields
.
:warning: Warning When a response is reduced, it will not call formatRows()
, unless the API's alwaysCallFormatter
getter returns true
This parameters will be passed to the model for reducing the response on the database-side.
For the following examples we will be using invented products with the information
[{
"id": 1,
"code": "t-shirt-red",
"name": "Shirt Red",
"price": 200.5,
"status": "active"
}, {
"id": 2,
"code": "t-shirt-blue",
"name": "Shirt Blue",
"price": 200.8,
"status": "active"
}]
Example: Reducing response with fields
When using fields
we are telling the database the specific fields we wanna receive in the response.
Important. When using fields
, excludeFields
will be ignored.
curl --location -g --request GET 'https://my-catalog.com/api/product?fields[0]=code&fields[1]=name'
// expected output: [{ id: 1, code: 't-shirt-red', name: 'Shirt Red' }, { id: 2, code: 't-shirt-blue', name: 'Shirt Blue' }]
Example: Reducing response with excludeFields
When using excludeFields
we are telling the database the specific fields we don't wanna receive in the response.
Important. When using fields
, excludeFields
will be ignored.
curl --location -g --request GET 'https://my-catalog.com/api/product?excludeFields[0]=price'
// expected output: [{ id: 1, code: 't-shirt-red', name: 'Shirt Red', status: 'active' }, { id: 2, code: 't-shirt-blue', name: 'Shirt Blue', status: 'active' }]
An ApiListData accepts request headers to modify default behavior.
Header | Description | Default |
---|
x-janis-page | Configure the page of the list to be consulted | 1 |
x-janis-page-size | The amount of rows to be returned. (max 100) | 60 |
x-janis-totals | The package will calculate total using getTotals() . Since 7.0.0. | false |
x-janis-only-totals | The package will calculate only total (no list items in response) using getTotals() . Since 7.0.0. | false |
ℹ️ The maximum page size can be modified with maxPageSize()
getter
An ApiListData will response the following headers.
Header | Description |
---|
x-janis-page | The page used to perform the get() command |
x-janis-page-size | The page size used in the get() command |
x-janis-total | The total of documents according the filters applied. Calculated with getTotals() |
ℹ️ The total calculation can be obtained using request header x-janis-totals or header x-janis-only-totals as true. Using header x-janis-only-totals will prevent using get()
command| and no list items will be returned
List APIs with parents
If you have for example, a list API for a sub-entity of one specific record, the parent will be automatically be added as a filter.
Important: The parent entity must be listed as an available filter
For example, the following endpoint: /api/parent-entity/1/sub-entity
, will be a list of the sub-entity, and parentEntity: '1'
will be set as a filter.
It could be thought as if it's equivalent to the following request: /api/sub-entity?filters[parentEntity]=1