hooks_use-hermes_query.js
import { isObject } from '@gravityforms/utils';
/**
* @function stringifyArg
* @description Build a string representation of a variable for use in Hermes query.
*
* @since 4.0.5
*
* @param {*} arg The property to convert to a string.
*
* @return {string} The string representation of the property.
*/
export const stringifyArg = ( arg ) => {
if ( arg === null || arg === undefined ) {
return 'null';
}
const type = typeof arg;
if ( type === 'string' ) {
// Stringify to escape special characters.
return JSON.stringify( arg );
} else if ( ! isObject( arg ) && ! Array.isArray( arg ) ) {
// Handle numbers, booleans, etc.
return `"${ String( arg ) }"`;
} else if ( Array.isArray( arg ) ) {
// Handle arrays
const elements = arg.map( ( e ) => stringifyArg( e ) );
return '[' + elements.join( ', ' ) + ']';
}
// Handle objects
const parts = [];
for ( const key in arg ) {
if ( Object.prototype.hasOwnProperty.call( arg, key ) ) {
const strKey = String( key );
const valueStr = stringifyArg( arg[ key ] );
parts.push( `${ strKey }: ${ valueStr }` );
}
}
return '{' + parts.join( ', ' ) + '}';
};
/**
* @function validateAliases
* @description Validates an array of objects to ensure three conditions related to `_alias` properties:
* 1. At least (array length - 1) objects must have an `_alias` key.
* 2. All `_alias` values must be unique.
* 3. None of the `_alias` values must match `key`.
*
* @param {Array} arr The array to validate.
* @param {string} key The key to avoid.
*
* @return {boolean} Whether the array passes validation or not.
*/
export const validateAliases = ( arr, key ) => {
const aliases = [];
for ( const obj of arr ) {
if ( Object.prototype.hasOwnProperty.call( obj, '_alias' ) ) {
const alias = obj._alias;
aliases.push( alias );
// Early exit if current alias already matches the key
if ( alias === key ) {
return false;
}
} else if ( Object.prototype.hasOwnProperty.call( obj, '_transform' ) ) {
const transformObj = obj._transform;
if ( Object.prototype.hasOwnProperty.call( transformObj, '_alias' ) ) {
const alias = transformObj._alias;
aliases.push( alias );
// Early exit if current alias already matches the key
if ( alias === key ) {
return false;
}
}
}
}
// Minimum count requirement
if ( aliases.length < ( arr.length - 1 ) ) {
return false;
}
// Check for key matches (redundant check but included for clarity)
if ( aliases.includes( key ) ) {
return false;
}
// Uniqueness check
const uniqueAliases = new Set( aliases );
return uniqueAliases.size === aliases.length;
};
/**
* @function buildArgsString
* @description Build an arguments string from an object for use in Hermes query.
*
* @since 4.1.0
*
* @param {object} args The object to convert to a string.
* @param {boolean} returnParensOnEmpty Whether to return parentheses around the args string if it is empty.
*
* @return {string} The arguments string.
*/
export const buildArgsString = ( args = {}, returnParensOnEmpty = false ) => {
const argsStr = Object.entries( args )
.map( ( [ argKey, argValue ] ) => `${ argKey }: ${ stringifyArg( argValue ) }` )
.join( ', ' );
if ( argsStr ) {
return `(${ argsStr })`;
}
return returnParensOnEmpty ? '()' : '';
};
/**
* @function buildQueryStringPart
* @description Build a query string part from a query object and key for use in Hermes query.
*
* @since 4.1.0
*
* @throws {Error} If transform does not have `_alias` and `_args` keys.
*
* @param {object} obj The query object to convert to a string.
* @param {string} key The key of the query object.
*
* @return {string} The query string part.
*/
export const buildQueryStringPart = ( obj, key ) => {
// If obj has only one key and the key is _alias, return the aliased key.
if ( Object.keys( obj ).length === 1 && obj._alias ) {
return `${ obj._alias }: ${ key }`;
}
// If obj has only one key and the key is _transform, return the transform query string part.
if ( Object.keys( obj ).length === 1 && obj._transform ) {
const transformObj = obj._transform;
if ( ! Object.prototype.hasOwnProperty.call( transformObj, '_alias' ) || ! Object.prototype.hasOwnProperty.call( transformObj, '_args' ) ) {
throw new Error( 'Transforms must have `_alias` and `_args` keys.' );
}
const argsPart = buildArgsString( transformObj._args, true );
const fieldName = `${ transformObj._alias }: ${ key }`;
return `${ fieldName }${ argsPart }`;
}
// Extract args from the object.
const args = obj._args || {};
const argsPart = buildArgsString( args );
// Extract alias from the object.
const fieldName = obj._alias ? `${ obj._alias }: ${ key }` : key;
// Build nested fields.
const nestedObj = {};
for ( const subKey in obj ) {
if ( ! subKey.startsWith( '_' ) ) {
nestedObj[ subKey ] = obj[ subKey ];
}
}
const nestedStr = buildQueryStringRecursive( nestedObj );
return `${ fieldName }${ argsPart } ${ nestedStr }`;
};
/**
* @function buildQueryStringRecursive
* @description Recursively build a query string from a query object for use in Hermes query.
*
* @since 4.0.5
*
* @throws {Error} If an array of queries for the same key do not have aliases.
*
* @param {object|Array} obj The query object or array to convert to a string.
*
* @return {string} The query string.
*/
export const buildQueryStringRecursive = ( obj = {} ) => {
const parts = [];
for ( const key in obj ) {
// Skip private properties.
if ( key.startsWith( '_' ) ) {
continue;
}
const value = obj[ key ];
if ( Array.isArray( value ) ) {
// Check that an alias exists for at least n-1 entries and are unique.
if ( ! validateAliases( value, key ) ) {
throw new Error( 'Aliases that do not match the key are required for an array of queries.' );
}
// Process each array item as separate field.
for ( const item of value ) {
parts.push( buildQueryStringPart( item, key ) );
}
} else if ( isObject( value ) ) {
// Handle nested objects (non-array, non-null).
parts.push( buildQueryStringPart( value, key ) );
} else if ( value === true ) {
parts.push( key );
}
}
return `{${ parts.join( ', ' ) }}`;
};
/**
* @function buildQueryString
* @description Build a query string from a query object for use in Hermes query.
*
* @since 4.0.5
*
* @param {object} queryObj The query object to convert to a string.
*
* @return {string} The query string.
*
* @example
* const queryObj = {
* company: {
* _alias: 'first_ten_companies',
* _args: {
* limit: 10,
* },
* id: true,
* name: {
* _alias: 'business_name',
* },
* address: true,
* department: {
* id: {
* _alias: 'department_id',
* },
* name: true,
* },
* },
* };
*
* const queryString = buildQueryString( queryObj );
* // queryString = '{first_ten_companies: company(limit: 10) {id, business_name: name, address, department {department_id: id, name}}}';
*
* const arrayQueryObj = {
* company: [
* {
* _args: {
* limit: 2,
* },
* id: true,
* },
* {
* _alias: 'pagination',
* _args: {
* limit: 1,
* },
* aggregate: true,
* },
* ],
* };
* const arrayQueryString = buildQueryString( arrayQueryObj );
* // arrayQueryString = '{company(limit: 2) {id}, pagination: company(limit: 1) {aggregate}}';
*
* const transformQueryObj = {
* company: {
* companyLogo: {
* _transform: {
* _alias: 'companyLogoSrc',
* _args: {
* transformMakeThumb: 'medium',
* },
* },
* },
* },
* };
* const transformQueryString = buildQueryString( transformQueryObj );
* // transformQueryString = '{company {companyLogoSrc: companyLogo(trahsformMakeThumb: "medium")}}';
*
*/
export const buildQueryString = ( queryObj = {} ) => {
const result = buildQueryStringRecursive( queryObj );
return result.trim();
};