@imqueue/graphql-dependency
Version:
Cross-service GraphQL dependencies for underlying @imqueue services
310 lines (309 loc) • 12.4 kB
TypeScript
/*!
* @imqueue/graphql-dependency - Declarative GraphQL dependency loading
*
* I'm Queue Software Project
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* If you want to use this code in a closed source (commercial) project, you can
* purchase a proprietary commercial license. Please contact us at
* <support@imqueue.com> to get commercial licensing options.
*/
import { GraphQLObjectType } from 'graphql';
import { DataInitializer, DataLoader, DependencyFieldsGetter, DependencyOptionsGetter } from './types';
/**
* Class GraphQLDependency
* Implements dependency relationship descriptions between different
* GraphQLObjectType entities, providing possibility of optimal data
* fetching during GraphQL queries resolution.
*
* Glossary:
* - Initializer: async routine required to fill up
* specific entity set. Can block deps loading
* before resolution if init fields are a part of
* dep loading process
* - Loader: async routine required to load dependency
* itself by a given filter using a given request
* fields.
*
* It implements cascade loading - analyze request fields against
* initial result and build load chains for the same type
* with merging all fields for the same type and load
* only missing objects on each iteration. Loading are performed
* as bulk operations using defined bulk data-loaders and initializers.
* It will try to perform as parallel as possible depending on the dependencies
* definition between objects.
*/
export declare class GraphQLDependency<ResultType> {
readonly type: GraphQLObjectType;
/**
* Creates dependency entity registering it with internal registry
* for further use. Use this method to construct entities as far as
* it will guarantee there is only one instance created for particular
* GraphQL object type, which it wraps on app initialization.
*
* @param {GraphQLObjectType} type
* @return {GraphQLDependency<TResultType>}
*/
static create<TResultType>(type: GraphQLObjectType): GraphQLDependency<TResultType>;
/**
* Checks if a given filter arg is empty or not
*
* @access private
* @param {any} filter
* @return {boolean}
*/
static isEmptyArg(filter: any): boolean;
private static deps;
private loader;
private init?;
private initFields;
private options;
private initFieldNames;
/**
* Class constructor
* @constructor
* @param {GraphQLObjectType} type - associated GraphQL type
*/
protected constructor(type: GraphQLObjectType);
/**
* Defines a loader for this particular dependency. This
* usually must be a bulk loading function accepting input of
* filters and returning appropriate data result type.
*
* Usually this can be defined close to the entity definition itself
* and signal that the entity may be a part of dependent structure for
* other top-level entities in the queries.
*
* @example
* ```typescript
* Dependency(User).defineLoader(async <User[]>(
* context: any,
* filter: FiltersInput,
* fields?: FieldsMapInput,
* ) => (await context.user.listUser(filter, fields)).data);
* ```
*
* @param {DataLoader<T>} loader
* @return {GraphQLDependency}
*/
defineLoader<T>(loader: DataLoader<T>): GraphQLDependency<ResultType>;
/**
* Defines an async initializer for this particular entity. Initializers are
* usually used to perform async routines required to pre-fill entity
* data before any other dependencies for this entity are loaded.
*
* Note: ite performs better when initializer fields are provided.
*
* @example
* ```typescript
* Dependency(UserType).defineInitializer(
* async <User[]>(
* context: any,
* result: User[],
* ) => {
* // do init stuff appending extra fields to
* // result set...
* // this will block deps loading related to
* // initializer fields or will block all deps loading
* // if initializer fields are not specified
* },
* User.getFields().orderId,
* User.getFields().shipmentIds,
* );
* ```
*
* @param {DataInitializer} initializer - async init routine to be used as
* entity initializer
* @param {...GraphQLField} [fields] - list of initializer fields it is
* linked to [optional].
* @return {GraphQLDependency}
*/
defineInitializer(initializer: DataInitializer<ResultType>, ...fields: DependencyFieldsGetter[]): GraphQLDependency<ResultType>;
/**
* Defines dependencies for this entity.
* Usually it lays along with the entity definition itself and describes
* how the particular entity should be fully resolved.
*
* @example
* ```typescript
* Dependency(CompanyType).require(UserType, () => ({
* as: CompanyType.getFields().employees
* filter: { [User.getFields().companyId.name]: Company.getFields().id }
* }), () => ({
* as: Company.getFields().owner,
* filter: { [UserType.getFields().id.name]: Company.getFields().ownerId }
* }));
* ```
*
* @param {GraphQLObjectType} child - child entity this entity depends on
* @param {DependencyOptions} options - options for dependency description
* @return {GraphQLDependency}
*/
require(child: GraphQLObjectType, ...options: DependencyOptionsGetter[]): GraphQLDependency<ResultType>;
/**
* Performs actual work on loading all entities current entity depends on.
* This will load all dependent entities using pre-defined bulk loaders
* and dependency configurations which were set by defineInitializer(),
* defineLoader() and require() calls on app startup. Loading is usually
* performed at runtime on particular graphql queries.
*
* During the execution it will analyze and call those bulk loaders
* which are required to pre-fill user data with related dependent
* structures. If some loaders may be need to called several times it
* will request only missing parts, those which was already pre-loaded
* would be re-used from previous load calls.
*
* Finally, this will be processed as:
* 1. Scan fields to identify which deps loaders requested to call
* 2. Scan deps and underlying types across fields and merge all
* similar-type request fields into minimal complete fields map
* definition
* 3. Scan and call for proper initializers and deps loaders in
* particular order
* 4. Re-map loaded data to result according requested by user fields
* 5. Return modified result
*
* @example
* ```typescript
* async buildResolutionCache(
* source: any,
* args: any,
* context: any,
* info: GraphQLResolveInfo,
* ) => { // imagine we are inside some query resolver
* const fields = fieldsMap(info);
* const userData = await context.userService.listAll();
*
* return await Dependency(User).load(userData, context, fields);
* }
* ```
*
* @param {any} source - source data object, usually obtained by some
* initial service call inside particular query
* resolver
* @param {any} context - execution context (usually passed to graphql
* resolver)
* @param {any} fields - requested fields as map object (usually can be
* obtained from GraphQLResolveInfo object passed
* to a query resolver and constructed using
* graphql-fields-map#fieldsMap() routine)
* @return {Promise<any>}
*/
load(source: ResultType, context: any, fields: any): Promise<ResultType>;
/**
* Resolves dependencies required to be loaded for a given user request
* identified by given request fields. Stores resolution result
* under given result map or will initializes new one and returns it.
* Result map usually is used by a recursive calls, so should not be passed
* on a top-level dependency call.
*
* @access private
* @param {any} fields - user requested fields
* @param {any} [source] - cached data if any
* @param {ResolutionCache} [cache] - resolution map to store result in
* @return {ResolutionCache} - resolution map and max call priority
*/
private buildResolutionCache;
/**
* Collects and returns call arguments for dependency loader from a given
* source data using dependency filtering argument options
*
* @access private
* @param {any} source - data source object
* @param {DependencyFilterOptions} filter - filter arguments lookup opts
* @param {ResolutionCache} cache - resolution cache
* @return {any} - call filtering argument
*/
private makeCallArgs;
/**
* Performs recursive incremental load of dependent data and map results to
* a given source object then returns it
*
* @access private
* @param {any} source
* @param {any} context
* @param {any} fields
* @param {ResolutionCache} cache
* @return {any}
*/
private incrementalLoad;
/**
* Builds and returns next callable level of source object for child
* dependencies
*
* @access private
* @param {any} source
* @param {string} field
* @return {any[]}
*/
private childSource;
/**
* Checks if given user requested fields need to be initialized before
* any dependency for current load level to be called (this usually
* occurs when dependency relies on initializer dependent fields)
*
* @access private
* @param {any} fields
* @param {GraphQLFieldMap<any, any, any>>} [gqlFields]
* @return {boolean}
*/
private waitForInit;
/**
* Performs initializer call and maps it's result to a given data source,
* returning modified source object.
*
* @access private
* @param {any} source - data source object
* @param {any} context - GraphQL request context
* @param {any} fields - user request fields map
* @param {ResolutionCache} cache - request resolution cache
* @return {any}
*/
private requestInitializer;
/**
* Performs recursive dependency loading and updates source object
* with loaded data
*
* @access private
* @param {any} source - data source object
* @param {any} context - graphql request context object
* @param {DependencyOptions} option - dependency options config
* @param {GraphQLDependency<any>} dep - loaded dependency object itself
* @param {ResolutionCache} cache - Resolution cache object
* @return {any}
*/
private requestLoader;
}
/**
* Imitates static constructor for GraphQLDependency class. Actually it is
* a short-hand alias to GraphQLDependency.create
*
* @see GraphQLDependency#create
*
* @example
* ```typescript
* Dependency(UserType).require(CompanyType, {
* as: UserType.getFields().company,
* filter: { // filter is used for company data loader
* [CompanyType.getFields().id.name]: UserType.getFields().companyId,
* },
* });
* ```
*
* @param {GraphQLObjectType}
* @return {GraphQLDependency}
*/
export declare const Dependency: typeof GraphQLDependency.create;