Helper functions for memoization, async handling, type guards, data structures, and performance monitoring. These utilities provide essential building blocks for efficient GraphQL operations and general-purpose data processing.
Function memoization utilities for performance optimization with different argument counts.
/**
* Memoize a function with one argument
* @param fn - Function to memoize
* @returns Memoized function
*/
function memoize1<TArgs, TReturn>(fn: (a1: TArgs) => TReturn): (a1: TArgs) => TReturn;
/**
* Memoize a function with two arguments
* @param fn - Function to memoize
* @returns Memoized function
*/
function memoize2<TArgs1, TArgs2, TReturn>(
fn: (a1: TArgs1, a2: TArgs2) => TReturn
): (a1: TArgs1, a2: TArgs2) => TReturn;
/**
* Memoize a function with three arguments
* @param fn - Function to memoize
* @returns Memoized function
*/
function memoize3<TArgs1, TArgs2, TArgs3, TReturn>(
fn: (a1: TArgs1, a2: TArgs2, a3: TArgs3) => TReturn
): (a1: TArgs1, a2: TArgs2, a3: TArgs3) => TReturn;
/**
* Memoize a function with four arguments
* @param fn - Function to memoize
* @returns Memoized function
*/
function memoize4<TArgs1, TArgs2, TArgs3, TArgs4, TReturn>(
fn: (a1: TArgs1, a2: TArgs2, a3: TArgs3, a4: TArgs4) => TReturn
): (a1: TArgs1, a2: TArgs2, a3: TArgs3, a4: TArgs4) => TReturn;
/**
* Memoize a function with five arguments
* @param fn - Function to memoize
* @returns Memoized function
*/
function memoize5<TArgs1, TArgs2, TArgs3, TArgs4, TArgs5, TReturn>(
fn: (a1: TArgs1, a2: TArgs2, a3: TArgs3, a4: TArgs4, a5: TArgs5) => TReturn
): (a1: TArgs1, a2: TArgs2, a3: TArgs3, a4: TArgs4, a5: TArgs5) => TReturn;
/**
* Memoize first 2 of 4 arguments of a function
* @param fn - Function to memoize
* @returns Memoized function
*/
function memoize2of4<F extends (a1: any, a2: any, a3: any, a4: any) => any>(fn: F): F;
/**
* Memoize first 2 of 5 arguments of a function
* @param fn - Function to memoize
* @returns Memoized function
*/
function memoize2of5<F extends (a1: any, a2: any, a3: any, a4: any, a5: any) => any>(fn: F): F;Usage Examples:
import { memoize1, memoize2, memoize3 } from "@graphql-tools/utils";
// Memoize expensive type lookup
const getTypeFromSchema = memoize2(
(schema: GraphQLSchema, typeName: string) => {
console.log(`Looking up type: ${typeName}`);
return schema.getType(typeName);
}
);
// Multiple calls with same arguments only execute once
const userType1 = getTypeFromSchema(schema, 'User'); // Executes function
const userType2 = getTypeFromSchema(schema, 'User'); // Returns cached result
// Memoize field collection
const collectFieldsMemoized = memoize3(
(schema: GraphQLSchema, selectionSet: SelectionSetNode, type: GraphQLObjectType) => {
return collectFields(schema, {}, {}, type, selectionSet);
}
);
// Memoize complex calculations
const calculateComplexMetric = memoize1((data: ComplexData) => {
console.log('Calculating complex metric...');
return performExpensiveCalculation(data);
});Intelligent deep merging for GraphQL objects and complex data structures.
/**
* Deep merge multiple objects with intelligent GraphQL-aware merging
* @param sources - Array of objects to merge
* @returns Merged object
*/
function mergeDeep<T>(sources: Partial<T>[]): T;Usage Examples:
import { mergeDeep } from "@graphql-tools/utils";
// Merge resolver objects
const baseResolvers = {
Query: {
user: () => ({ id: '1', name: 'John' }),
posts: () => []
}
};
const extendedResolvers = {
Query: {
posts: () => [{ id: '1', title: 'Hello' }],
comments: () => []
},
User: {
posts: (user) => findPostsByUser(user.id)
}
};
const mergedResolvers = mergeDeep([baseResolvers, extendedResolvers]);
// Result: Query has user, posts (from extended), and comments
// User resolver is added
// Merge schema configurations
const baseConfig = {
typeDefs: baseTypes,
resolvers: baseResolvers,
options: {
cacheControl: true
}
};
const additionalConfig = {
resolvers: additionalResolvers,
options: {
tracing: true,
introspection: true
}
};
const finalConfig = mergeDeep([baseConfig, additionalConfig]);
// Result: Combined typeDefs, merged resolvers, merged optionsUtilities for working with async iterators, observables, and promise handling.
/**
* Type guard to check if a value is an async iterable
* @param value - Value to check
* @returns True if value is async iterable
*/
function isAsyncIterable(value: any): value is AsyncIterable<unknown>;
/**
* Convert an Observable to an AsyncIterable
* @param observable - Observable to convert
* @returns AsyncIterable that yields observable values
*/
function observableToAsyncIterable<T>(observable: Observable<T>): AsyncIterable<T>;
/**
* Add cancellation capability to async operations
* @param asyncIterable - The async iterable to make cancellable
* @param onCancel - Callback called when cancelled
* @returns Cancellable async iterable
*/
function withCancel<T>(
asyncIterable: AsyncIterable<T>,
onCancel: () => void
): AsyncIterable<T>;
/**
* Reduce an array with async operations, processing sequentially
* @param values - Array of values to reduce
* @param callback - Async reducer function
* @param initial - Initial accumulator value
* @returns Promise resolving to final accumulated value
*/
function promiseReduce<T, R>(
values: T[],
callback: (accumulator: R, currentValue: T, currentIndex: number) => Promise<R>,
initial: R
): Promise<R>;
/**
* Register a listener for abort signals with cleanup
* @param signal - AbortSignal to listen to
* @param listener - Function called when aborted
* @returns Cleanup function to remove listener
*/
function registerAbortSignalListener(
signal: AbortSignal,
listener: () => void
): () => void;Usage Examples:
import {
isAsyncIterable,
observableToAsyncIterable,
withCancel,
promiseReduce,
registerAbortSignalListener
} from "@graphql-tools/utils";
// Check for async iterable support
async function processData(data: unknown) {
if (isAsyncIterable(data)) {
for await (const item of data) {
console.log('Processing item:', item);
}
} else {
console.log('Data is not async iterable');
}
}
// Convert observable to async iterable for GraphQL subscriptions
const observable = new Observable(subscriber => {
let count = 0;
const interval = setInterval(() => {
subscriber.next({ count: count++ });
}, 1000);
return () => clearInterval(interval);
});
const asyncIterable = observableToAsyncIterable(observable);
// Add cancellation support
const cancellableIterable = withCancel(asyncIterable, () => {
console.log('Subscription cancelled');
});
// Use in GraphQL subscription resolver
const subscriptionResolver = {
Subscription: {
counter: {
subscribe: () => cancellableIterable
}
}
};
// Sequential async processing
const userIds = ['1', '2', '3', '4'];
const processedUsers = await promiseReduce(
userIds,
async (acc, userId) => {
const user = await fetchUser(userId);
return [...acc, { ...user, processed: true }];
},
[]
);
// Handle abort signals
const abortController = new AbortController();
const cleanup = registerAbortSignalListener(
abortController.signal,
() => {
console.log('Operation was aborted');
// Cleanup resources
}
);
// Later: abortController.abort() will trigger cleanupType guard functions for runtime type checking and validation.
/**
* Check if a value is a GraphQL DocumentNode
* @param value - Value to check
* @returns True if value is a DocumentNode
*/
function isDocumentNode(value: any): value is DocumentNode;
/**
* Check if a value is object-like (object but not null)
* @param value - Value to check
* @returns True if value is object-like
*/
function isObjectLike(value: any): value is Record<string, any>;
/**
* Check if a value is a Promise
* @param value - Value to check
* @returns True if value is a Promise
*/
function isPromise(value: any): value is Promise<any>;
/**
* Check if a value matches specified criteria
* @param value - Value to test
* @param criteria - Criteria to match against
* @returns True if value matches criteria
*/
function valueMatchesCriteria(value: any, criteria: any): boolean;Usage Examples:
import {
isDocumentNode,
isObjectLike,
isPromise,
valueMatchesCriteria
} from "@graphql-tools/utils";
// Validate GraphQL documents
function processDocument(input: unknown) {
if (isDocumentNode(input)) {
console.log('Processing GraphQL document with', input.definitions.length, 'definitions');
return input;
}
throw new Error('Invalid document provided');
}
// Safe object property access
function getProperty(obj: unknown, key: string) {
if (isObjectLike(obj)) {
return obj[key];
}
return undefined;
}
// Handle async/sync values uniformly
async function normalizeValue<T>(value: T | Promise<T>): Promise<T> {
if (isPromise(value)) {
return await value;
}
return value;
}
// Flexible value matching
const users = [
{ name: 'John', age: 30, active: true },
{ name: 'Jane', age: 25, active: false },
{ name: 'Bob', age: 35, active: true }
];
// Find users matching criteria
const activeAdults = users.filter(user =>
valueMatchesCriteria(user, { active: true }) &&
user.age >= 18
);Specialized data structures for GraphQL operations and general use.
/**
* Specialized Map for accumulating arrays of values by key
*/
class AccumulatorMap<T> extends Map<string, T[]> {
/**
* Add an item to the array for the specified key
* @param key - Key to add item under
* @param item - Item to add to the array
*/
add(key: string, item: T): void;
/**
* Get all items for a key, returning empty array if key doesn't exist
* @param key - Key to get items for
* @returns Array of items for the key
*/
get(key: string): T[];
}Usage Examples:
import { AccumulatorMap } from "@graphql-tools/utils";
// Track field selections by type
const fieldSelections = new AccumulatorMap<string>();
// Add selections for different types
fieldSelections.add('User', 'id');
fieldSelections.add('User', 'name');
fieldSelections.add('User', 'email');
fieldSelections.add('Post', 'title');
fieldSelections.add('Post', 'content');
// Get all fields for a type
console.log('User fields:', fieldSelections.get('User'));
// Result: ['id', 'name', 'email']
console.log('Post fields:', fieldSelections.get('Post'));
// Result: ['title', 'content']
// Check if any fields selected for a type
if (fieldSelections.get('Comment').length === 0) {
console.log('No fields selected for Comment type');
}
// Use in GraphQL resolver optimization
const resolveUser = (source, args, context, info) => {
const selections = new AccumulatorMap<string>();
// Collect all selected fields
collectFields(info.schema, {}, {}, info.returnType, info.fieldNodes[0].selectionSet)
.fields.forEach((fieldNodes, fieldName) => {
selections.add('User', fieldName);
});
// Optimize database query based on selections
const selectedFields = selections.get('User');
return context.db.findUser(args.id, { fields: selectedFields });
};Tools for performance monitoring and debugging GraphQL operations.
/**
* Create a debug timer for measuring execution duration
* @param label - Label for the timer
* @returns Function to call when operation completes
*/
function debugTimer(label: string): () => void;
/**
* Object inspection utility with GraphQL-aware formatting
* @param object - Object to inspect
* @param options - Inspection options
* @returns Formatted string representation
*/
function inspect(object: any, options?: InspectOptions): string;
interface InspectOptions {
depth?: number;
colors?: boolean;
compact?: boolean;
breakLength?: number;
}Usage Examples:
import { debugTimer, inspect } from "@graphql-tools/utils";
// Measure resolver performance
const resolveUser = async (source, args, context, info) => {
const timer = debugTimer(`User resolver for ID: ${args.id}`);
try {
const user = await context.db.findUser(args.id);
return user;
} finally {
timer(); // Logs execution time
}
};
// Measure schema transformation
const transformSchema = (schema) => {
const timer = debugTimer('Schema transformation');
const transformed = mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
// Transform field
return fieldConfig;
}
});
timer(); // Logs transformation time
return transformed;
};
// Debug complex objects
const debugResolver = (source, args, context, info) => {
console.log('Resolver called with:');
console.log('Args:', inspect(args, { depth: 3, colors: true }));
console.log('Context keys:', inspect(Object.keys(context)));
console.log('Info path:', inspect(info.path));
return resolveData(args);
};
// Performance monitoring for field collection
const collectFieldsWithTiming = (schema, fragments, variables, type, selectionSet) => {
const timer = debugTimer('Field collection');
const result = collectFields(schema, fragments, variables, type, selectionSet);
timer();
console.log('Collected fields:', inspect(Object.keys(result.fields)));
return result;
};Additional utility functions for common GraphQL operations.
/**
* Get description from an object, handling both string and object formats
* @param object - Object that may contain description
* @returns Description string or undefined
*/
function descriptionFromObject(object: any): string | undefined;
/**
* Get leading comment block from AST node
* @param node - AST node to extract comment from
* @returns Comment text or undefined
*/
function getLeadingCommentBlock(node: ASTNode): string | undefined;
/**
* Get trailing comment block from AST node
* @param node - AST node to extract comment from
* @returns Comment text or undefined
*/
function getTrailingCommentBlock(node: ASTNode): string | undefined;
/**
* Update an argument in a field configuration
* @param fieldConfig - Field configuration to update
* @param argName - Name of argument to update
* @param argUpdate - New argument configuration
* @returns Updated field configuration
*/
function updateArgument(
fieldConfig: GraphQLFieldConfig<any, any>,
argName: string,
argUpdate: GraphQLArgumentConfig
): GraphQLFieldConfig<any, any>;
/**
* Transform input values in schema recursively
* @param schema - Schema to transform
* @param transform - Transformation function
* @returns Schema with transformed input values
*/
function transformInputValue(
schema: GraphQLSchema,
transform: (type: GraphQLInputType, value: any) => any
): GraphQLSchema;Usage Examples:
import {
descriptionFromObject,
getLeadingCommentBlock,
updateArgument,
transformInputValue
} from "@graphql-tools/utils";
// Extract descriptions from various sources
const typeDescription = descriptionFromObject(typeDefinition);
console.log('Type description:', typeDescription);
// Extract comments from AST nodes
const comment = getLeadingCommentBlock(fieldDefinitionNode);
if (comment) {
console.log('Field comment:', comment);
}
// Update field arguments
const updatedField = updateArgument(
userField,
'limit',
{
type: GraphQLInt,
defaultValue: 10,
description: 'Maximum number of results'
}
);
// Transform default values in schema
const schemaWithTransformedDefaults = transformInputValue(
schema,
(type, value) => {
if (type === GraphQLString && typeof value === 'string') {
return value.trim().toLowerCase();
}
return value;
}
);More utility functions for common GraphQL and general programming tasks.
/**
* Checks if the given string is a valid URL
* @param str - The string to validate as a URL
* @returns Boolean indicating whether the string is a valid URL
*/
function isUrl(str: string): boolean;
/**
* Ensure a value is an array
* @param fns - Value that may be single item or array
* @returns Array containing the value(s)
*/
function asArray<T>(fns: T | T[]): T[];
/**
* Determines if a given input is a valid GraphQL document string
* @param str - The input to validate as a GraphQL document
* @returns Boolean indicating whether the input is a valid GraphQL document string
*/
function isDocumentString(str: any): boolean;
/**
* Checks whether the string contains any path illegal characters
* @param str - String to validate as a file path
* @returns Boolean indicating if string is a valid path
*/
function isValidPath(str: any): boolean;
/**
* Compare two strings alphabetically
* @param a - First string to compare
* @param b - Second string to compare
* @returns -1, 0, or 1 based on comparison
*/
function compareStrings<A, B>(a: A, b: B): number;
/**
* Convert AST node to string representation
* @param a - AST node to convert
* @returns String representation of the node
*/
function nodeToString(a: ASTNode): string;
/**
* Compare two AST nodes
* @param a - First AST node
* @param b - Second AST node
* @param customFn - Optional custom comparison function
* @returns Comparison result
*/
function compareNodes(a: ASTNode, b: ASTNode, customFn?: (a: any, b: any) => number): number;
/**
* Type guard to check if value is not null or undefined
* @param input - Value to check
* @returns True if value is not null/undefined
*/
function isSome<T>(input: T): input is Exclude<T, null | undefined>;
/**
* Assert that value is not null or undefined
* @param input - Value to assert
* @param message - Optional error message
* @throws Error if value is null or undefined
*/
function assertSome<T>(
input: T,
message?: string
): asserts input is Exclude<T, null | undefined>;
/**
* Check if a value is iterable
* @param value - Value to check
* @returns True if value is iterable
*/
function isIterableObject(value: unknown): value is Iterable<unknown>;
/**
* Safe hasOwnProperty check
* @param obj - Object to check property on
* @param prop - Property name to check
* @returns True if object has the property
*/
function hasOwnProperty(obj: unknown, prop: string): boolean;
/**
* Get description node from an object
* @param obj - Object that may contain description
* @returns Description string node or undefined
*/
function getDescriptionNode(obj: any): StringValueNode | undefined;Usage Examples:
import {
isUrl,
asArray,
isDocumentString,
isSome,
assertSome,
hasOwnProperty
} from "@graphql-tools/utils";
// URL validation
if (isUrl("https://example.com/graphql")) {
console.log("Valid GraphQL endpoint URL");
}
// Ensure array format
const resolvers = asArray(singleResolver); // Always returns array
// Validate GraphQL document strings
if (isDocumentString(userInput)) {
const document = parse(userInput);
console.log("Valid GraphQL document");
}
// Safe null checks
const user = getUser();
if (isSome(user)) {
// TypeScript knows user is not null here
console.log(user.name);
}
// Assertion with better error messages
assertSome(config.apiKey, "API key is required for authentication");
// Safe property access
if (hasOwnProperty(data, 'fieldName')) {
console.log('Field exists:', data.fieldName);
}