drupal-jsonapi-params
Version:
Drupal JSON-API params
438 lines (437 loc) • 14.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DrupalJsonApiParams = void 0;
const qs = require("qs");
class DrupalJsonApiParams {
/**
* Optionaly initialize with a previously stored query/object/query string.
*
* @category Init
*/
constructor(input, config) {
this.data = {
filter: {},
include: [],
page: undefined,
sort: [],
fields: {},
};
this.qsOptions = {};
this.config = {
useShortCutForQueryGeneration: true,
alwaysUseFieldNameForKeys: false,
};
if (config !== undefined) {
this.config = config;
}
this.initialize(input);
}
/**
* Add custom parameter to the query.
*
* E.g. usage
*
* ```js
* apiParams
* // To add `foo=bar` to the query.
* .addCustomParam({foo: 'bar'})
* // To add `foo[bar]=baz` to the query.
* .addCustomParam({ foo: {bar: 'baz'}})
* // To add `bar[0]=a&bar[1]=b&bar[2]=c` to the query.
* .addCustomParam({ bar: ['a', 'b', 'c']})
* ```
*
* @param input The parameter object
*
* @category Helper
*/
addCustomParam(input) {
this.data = Object.assign(Object.assign({}, this.data), input);
return this;
}
/**
* Add JSON:API field.
*
* The name of this method might be miss leading. Use this to explicitely request for specific fields on an entity.
*
* @param type Resource type
* @param fields Array of field names in the given resource type
*
* @category JSON:API Query
*/
addFields(type, fields) {
this.data.fields[type] = fields.join(',');
return this;
}
/**
* Add JSON:API sort.
*
* Used to return the list of items in specific order.
*
* [Read more about Sort in Drupal.org Documentation](https://www.drupal.org/docs/8/modules/jsonapi/sorting)
*
* @param path A 'path' identifies a field on a resource
* @param direction Sort direction `ASC` or `DESC`
*
* @category JSON:API Query
*/
addSort(path, direction) {
let prefix = '';
if (direction !== undefined && direction === 'DESC') {
prefix = '-';
}
this.data.sort = this.data.sort.concat(prefix + path);
return this;
}
/**
* Add JSON:API page limit.
*
* Use to restrict max amount of items returned in the listing.
* Using this for pagination is tricky, and make sure you read
* the following document on Drupal.org to implement it correctly.
*
* [Read more about Pagination in Drupal.org Documentation](https://www.drupal.org/docs/8/core/modules/jsonapi-module/pagination)
*
* @param limit Number of items to limit to
*
* @category JSON:API Query
*/
addPageLimit(limit) {
if (this.data.page === undefined) {
this.data.page = { limit };
}
else {
this.data.page.limit = limit;
}
return this;
}
/**
* Add JSON:API page offset.
*
* Use to skip some items from the start of the listing.
* Using this for pagination is tricky, and make sure you read
* the following document on Drupal.org to implement it correctly.
*
* [Read more about Pagination in Drupal.org Documentation](https://www.drupal.org/docs/8/core/modules/jsonapi-module/pagination)
*
* @param offset Number of items to skip from the begining.
*
* @category JSON:API Query
*/
addPageOffset(offset) {
if (this.data.page === undefined) {
this.data.page = { offset };
}
else {
this.data.page.offset = offset;
}
return this;
}
/**
* Add JSON:API include.
*
* Used to add referenced resources inside same request.
* Thereby preventing additional api calls.
*
* [Read more about Includes in Drupal.org Documentation](https://www.drupal.org/docs/8/modules/jsonapi/includes)
*
* @param fields Array of field names
*
* @category JSON:API Query
*/
addInclude(fields) {
this.data.include = this.data.include.concat(fields);
return this;
}
/**
* Add JSON:API group.
*
* @param name Name of the group
* @param conjunction All groups have conjunctions and a conjunction is either `AND` or `OR`.
* @param memberOf Name of the group, this group belongs to
*
* @category JSON:API Query
*/
addGroup(name, conjunction = 'OR', memberOf) {
this.data.filter[name] = {
group: Object.assign({ conjunction }, (memberOf !== undefined && { memberOf })),
};
return this;
}
/**
* Add JSON:API filter.
*
* Following values can be used for the operator. If none is provided, it assumes "`=`" by default.
* ```
* '=', '<>',
* '>', '>=', '<', '<=',
* 'STARTS_WITH', 'CONTAINS', 'ENDS_WITH',
* 'IN', 'NOT IN',
* 'BETWEEN', 'NOT BETWEEN',
* 'IS NULL', 'IS NOT NULL'
* ```
*
* **NOTE: Make sure you match the value supplied based on the operators used as per the table below**
*
* | Value Type | Operator | Comment |
* | --- | --- | --- |
* | `string` | `=`, `<>`, `>`, `>=`, `<`, `<=`, `STARTS_WITH`, `CONTAINS`, `ENDS_WITH` | |
* | `string[]` | `IN`, `NOT IN` | |
* | `string[]` _size 2_ | `BETWEEN`, `NOT BETWEEN` | The first item is used for min (start of the range), and the second item is used for max (end of the range).
* | `null` | `IS NULL`, `IS NOT NULL` | Must use `null`
*
* [Read more about filter in Drupal.org Documentation](https://www.drupal.org/docs/8/core/modules/jsonapi-module/filtering)
*
* @param path A 'path' identifies a field on a resource
* @param value string[] | null` | A 'value' is the thing you compare against. For operators like "IN" which supports multiple parameters, you can supply an array.
* @param operator An 'operator' is a method of comparison
* @param memberOf Name of the group, the filter belongs to
*
* @category JSON:API Query
*/
addFilter(path, value, operator = '=', memberOf, key) {
const name = this.getIndexId(this.data.filter, key || path, !!key);
// Instead of relying on users supplying 'null' value, we
// hardcode value to 'null'. This should improve DX and be
// in line with how Condition query works in Drupal's PHP api.
if (operator === 'IS NULL' || operator === 'IS NOT NULL') {
value = null;
}
// Allow null values only for IS NULL and IS NOT NULL operators.
if (value === null) {
if (!(operator === 'IS NULL' || operator === 'IS NOT NULL')) {
throw new TypeError(`Value cannot be null for the operator "${operator}"`);
}
this.data.filter[name] = {
condition: Object.assign(Object.assign({ path }, { operator }), (memberOf !== undefined && { memberOf })),
};
return this;
}
if (Array.isArray(value)) {
switch (operator) {
case 'BETWEEN':
case 'NOT BETWEEN':
if (value.length !== 2) {
throw new TypeError(`Value must consists of 2 items for the "${operator}"`);
}
break;
case 'IN':
case 'NOT IN':
break;
default:
throw new TypeError(`Value cannot be an array for the operator "${operator}"`);
}
this.data.filter[name] = {
condition: Object.assign(Object.assign({ path,
value }, { operator }), (memberOf !== undefined && { memberOf })),
};
return this;
}
// Validate filter
if (this.config.useShortCutForQueryGeneration &&
memberOf === undefined &&
path === name &&
this.data.filter[path] === undefined) {
if (operator === '=') {
this.data.filter[name] = value;
}
else {
this.data.filter[name] = {
value,
operator,
};
}
return this;
}
this.data.filter[name] = {
condition: Object.assign(Object.assign({ path,
value }, (this.config.useShortCutForQueryGeneration ? operator !== '=' && { operator } : { operator })), (memberOf !== undefined && { memberOf })),
};
return this;
}
/**
* Generate a unique key name for the given object.
*
* @param obj The object to generate a key name for.
* @param proposedKey The proposed key name.
* @param enforceKeyName Whether to enforce the key name.
*
* @returns The generated key name.
*/
getIndexId(obj, proposedKey, enforceKeyName) {
enforceKeyName = enforceKeyName || this.config.alwaysUseFieldNameForKeys;
let key;
if (obj[proposedKey] === undefined) {
key = proposedKey;
}
else {
key = this.generateKeyName(obj, proposedKey, enforceKeyName);
}
return key;
}
/**
* Generate a unique key name for the given object.
*
* @param obj The object to generate a key name for.
* @param proposedKey The proposed key name.
* @param enforceKeyName Whether to enforce the key name.
*
* @returns The generated key name.
*/
generateKeyName(obj, proposedKey, enforceKeyName = false) {
const length = Object.keys(obj).length;
if (enforceKeyName) {
for (let ndx = 1; ndx <= length; ndx++) {
const key = `${proposedKey}--${ndx}`;
if (obj[key] === undefined) {
return key;
}
}
}
return length.toString();
}
/**
* Get query object.
*
* @category Helper
*/
getQueryObject() {
const foo = JSON.parse(JSON.stringify(this.data));
if (this.data.include.length > 0) {
foo.include = this.data.include.join(',');
}
else {
delete foo.include;
}
if (this.data.sort.length > 0) {
foo.sort = this.data.sort.join(',');
}
else {
delete foo.sort;
}
return foo;
}
/**
* Get query string.
*
* @param options Options to be passed to `qs` library during parsing.
*
* @category Helper
*/
getQueryString(options) {
const data = this.getQueryObject();
// NOTE: Empty objects are falsy in JavaScript.
const qsOptions = options || this.getQsOption();
return qs.stringify(data, qsOptions);
}
/**
* Clear all parameters added so far.
*
* @category Helper
*/
clear() {
this.data = {
filter: {},
include: [],
page: undefined,
sort: [],
fields: {},
};
return this;
}
/**
* Initialize with a previously stored query object.
*
* @category Init
*/
initializeWithQueryObject(input) {
this.clear();
const keys = Object.keys(input);
keys.forEach(key => {
switch (key) {
case 'sort':
if (input.sort.length) {
this.data.sort = input.sort.split(',');
}
break;
case 'include':
if (input.include.length) {
this.data.include = input.include.split(',');
}
break;
default:
this.data[key] = input[key];
}
});
return this;
}
/**
* Initialize with a previously stored query string.
*
* @param input The Query string to use for initializing.
* @param options Options to be passed to `qs` library during parsing.
*
* @category Init
*/
initializeWithQueryString(input, options) {
this.clear();
// NOTE: Empty objects are falsy in JavaScript.
const qsOptions = options || this.getQsOption();
this.initializeWithQueryObject(qs.parse(input, qsOptions));
return this;
}
/**
* Clone a given DrupalJsonApiParam object.
*
* @category Helper
*/
clone(input) {
const data = JSON.parse(JSON.stringify(input.getQueryObject()));
this.initializeWithQueryObject(data);
return this;
}
/**
* Set options that is passed to qs when parsing/serializing.
*
* @see https://www.npmjs.com/package/qs
*/
setQsOption(options) {
this.qsOptions = options;
return this;
}
/**
* Get options that is passed to qs when parsing/serializing.
*
* @see https://www.npmjs.com/package/qs
*/
getQsOption() {
return this.qsOptions;
}
/**
* Initialize with a previously stored query/object/query string.
*
* @category Init
*/
initialize(input) {
if (input === undefined) {
this.initializeWithQueryString('');
}
else if (typeof input === 'object') {
try {
// if the input has getQueryObject() we attempt to clone.
input.getQueryObject();
this.clone(input);
}
catch (error) {
// In any case if cloning failed, we attempt to initialize
// with query object.
this.initializeWithQueryObject(input);
}
}
else {
this.initializeWithQueryString(input);
}
return this;
}
}
exports.DrupalJsonApiParams = DrupalJsonApiParams;