hooks_use-hermes.js

import { React } from '@gravityforms/libraries';
import { post } from '@gravityforms/request';
import { isObject } from '@gravityforms/utils';
import { buildQueryString } from './use-hermes/query';
import { buildMutationString, mutationTypes } from './use-hermes/mutation';

const { useCallback, useState } = React;

/**
 * @module useHermes
 * @description A hook to interact with the Hermes API.
 *
 * @since 4.0.5
 *
 * @param {object} props                       The props for the hook.
 * @param {string} props.action                The fallback action to use if `queryAction` or `mutationAction` is not provided.
 * @param {object} props.customMutationTypeMap A map of custom mutation types to mutation string builder functions.
 * @param {string} props.endpoint              The Hermes AJAX endpoint.
 * @param {string} props.mutationAction        The mutation action to use.
 * @param {string} props.mutationKey           The key in the request body for mutations.
 * @param {string} props.mutationSecurity      The security nonce for mutations.
 * @param {string} props.queryAction           The query action to use.
 * @param {string} props.queryKey              The key in the request body for queries.
 * @param {string} props.querySecurity         The security nonce for queries.
 * @param {string} props.security              The security nonce to use if `querySecurity` or `mutationSecurity` is not provided.
 *
 * @return {object} The Hermes state and actions.
 */
const useHermes = ( {
	action = '',
	customMutationTypeMap = {},
	endpoint = '',
	mutationAction = '',
	mutationKey = 'mutation',
	mutationSecurity = '',
	queryAction = '',
	queryKey = 'query',
	querySecurity = '',
	security = '',
} ) => {
	const [ activeRequests, setActiveRequests ] = useState( 0 );

	if ( ! queryAction && ! action ) {
		throw new Error( 'A `queryAction` or `action` is required.' );
	}
	if ( ! mutationAction && ! action ) {
		throw new Error( 'A `mutationAction` or `action` is required.' );
	}

	const qAction = queryAction || action;
	const mAction = mutationAction || action;
	const qSecurity = querySecurity || security;
	const mSecurity = mutationSecurity || security;

	/**
	 * @function query
	 * @description Performs query on the Hermes API.
	 *
	 * @since 4.0.5
	 *
	 * @async
	 *
	 * @param {object}        args          Args for the query.
	 * @param {string|object} args.queryObj The query object.
	 * @param {object}        args.headers  The headers for the query.
	 * @param {object}        args.body     The body for the query.
	 * @param {object}        args.options  The options for the query.
	 *
	 * @return {Promise} The query promise.
	 */
	const query = useCallback( async ( {
		queryObj,
		headers = {},
		body = {},
		options = {},
	} ) => {
		// Validate the query object. Must be a string or an object.
		if ( typeof queryObj !== 'string' && ! isObject( queryObj ) ) {
			throw new Error( 'Invalid query parameter.' );
		}

		// Build the query string.
		let queryString = '';
		if ( typeof queryObj === 'string' ) {
			queryString = queryObj;
		} else if ( isObject( queryObj ) ) {
			queryString = buildQueryString( queryObj );
		}

		// Increment active requests.
		setActiveRequests( ( prev ) => prev + 1 );

		const response = await post( {
			endpoint,
			headers,
			body: {
				...body,
				action: qAction,
				security: qSecurity,
				[ queryKey ]: queryString,
			},
			options,
		} );

		setActiveRequests( ( prev ) => prev - 1 );

		return response;
	}, [ endpoint, qAction, qSecurity, queryKey ] );

	/**
	 * @function mutation
	 * @description Performs mutation on the Hermes API.
	 *
	 * @since 4.0.5
	 *
	 * @async
	 *
	 * @param {object}        args             Args for the mutation.
	 * @param {string|object} args.mutationObj The mutation object.
	 * @param {string}        args.type        The mutation type, one of the base types of `insert`, `update`, `delete`, `connect`, or `disconnect`, or a custom mutation type.
	 * @param {object}        args.headers     The headers for the mutation.
	 * @param {object}        args.body        The body for the mutation.
	 * @param {object}        args.options     The options for the mutation.
	 *
	 * @return {Promise} The mutation promise.
	 */
	const mutation = useCallback( async ( {
		mutationObj,
		type,
		headers = {},
		body = {},
		options = {},
	} ) => {
		// Validate the mutation object. Must be a string or an object.
		if ( typeof mutationObj !== 'string' && ! isObject( mutationObj ) ) {
			throw new Error( 'Invalid mutation parameter.' );
		}

		// Build the mutation string.
		let mutationString = '';
		if ( typeof mutationObj === 'string' ) {
			mutationString = mutationObj;
		} else if ( mutationTypes.includes( type ) ) {
			mutationString = buildMutationString( mutationObj, type );
		} else if ( customMutationTypeMap[ type ] ) {
			mutationString = customMutationTypeMap[ type ]( mutationObj );
		} else {
			throw new Error( 'Invalid mutation type' );
		}

		// Increment active requests.
		setActiveRequests( ( prev ) => prev + 1 );

		const response = await post( {
			endpoint,
			headers,
			body: {
				...body,
				action: mAction,
				security: mSecurity,
				[ mutationKey ]: mutationString,
			},
			options,
		} );
		setActiveRequests( ( prev ) => prev - 1 );

		return response;
	}, [ Object.keys( customMutationTypeMap ).join(), endpoint, mAction, mSecurity, mutationKey ] ); // eslint-disable-line react-hooks/exhaustive-deps

	return {
		isLoading: activeRequests > 0,
		query,
		mutation,
	};
};

export default useHermes;