@nestjs/graphql
Version:
Nest - modern, fast, powerful node.js web framework (@graphql)
138 lines (137 loc) • 6.84 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResolverImplementationMap = void 0;
const common_1 = require("@nestjs/common");
const is_throwing_util_1 = require("../utils/is-throwing.util");
/**
* Resolves which resolver class implements the handlers declared on a base class.
*
* A handler is recorded against the class it is declared on, but Nest instantiates the class
* registered as a provider. When a resolver extends a base class, every handler declared on that
* base therefore has to be reassigned to the derived class:
*
* class BaseUserResolver { @Query(() => User) user() {} } // target: BaseUserResolver
* @Resolver() class UserResolver extends BaseUserResolver {} // the class Nest instantiates
*
* so `user` has to end up targeting `UserResolver`. Note that the base class need not be a resolver
* itself - `@Query` records whichever class the method is written on, decorated or not.
*/
class ResolverImplementationMap {
/**
* @param resolvers every registered resolver class, in registration order.
* @param handlerGroups the handler arrays, kept as groups rather than a flattened set so they are
* only scanned when an ambiguity actually has to be reported.
*/
constructor(resolvers, handlerGroups) {
this.handlerGroups = handlerGroups;
this.resolverByBaseClass = new Map();
this.logger = new common_1.Logger(ResolverImplementationMap.name);
this.collectExtendedResolversMap(resolvers);
}
/** True when no resolver extends anything, so there is nothing to reassign. */
get isEmpty() {
return this.resolverByBaseClass.size === 0;
}
/**
* The resolver that ends up serving handlers declared on `baseClass`, following the full chain.
* `resolverByBaseClass` is keyed by base class and holds the resolver extending it, so a lookup
* steps one level *down* the chain. Keep stepping until nothing extends the class just landed on.
* Adding `class AdminUserResolver extends UserResolver` to the example above:
*
* BaseUserResolver -> UserResolver -> AdminUserResolver
*
* and `AdminUserResolver` serves it. Intermediate classes are stepped through, not selected.
*/
getResolverImplementationForBaseClass(baseClass) {
let resolver = this.resolverByBaseClass.get(baseClass);
while (resolver && this.resolverByBaseClass.has(resolver.target)) {
resolver = this.resolverByBaseClass.get(resolver.target);
}
return resolver;
}
/** Rewrites every metadata entry to target the resolver class that implements it, if any. */
collectAllMetadata(baseMetadatas) {
return baseMetadatas.map((baseMetadata) => this.getImplementationMetadataForBaseClass(baseMetadata));
}
/**
* Rewrites every field resolver metadata entry to target the resolver class that implements it,
* if any. A field resolver whose own `objectTypeFn` cannot resolve its host type falls back to
* the `typeFn` of the resolver that now owns it.
*/
collectFieldResolversMetadata(fieldResolvers) {
return fieldResolvers.map((metadata) => {
const classMetadata = this.getResolverImplementationForBaseClass(metadata.target);
if (!classMetadata) {
return metadata;
}
return {
...metadata,
target: classMetadata.target,
classMetadata,
objectTypeFn: (0, is_throwing_util_1.isThrowing)(metadata.objectTypeFn)
? classMetadata.typeFn
: metadata.objectTypeFn,
};
});
}
/** The same handler, reattributed to the resolver class that implements it. */
getImplementationMetadataForBaseClass(baseClassMetadata) {
const classMetadata = this.getResolverImplementationForBaseClass(baseClassMetadata.target);
return classMetadata
? { ...baseClassMetadata, target: classMetadata.target, classMetadata }
: baseClassMetadata;
}
/** Maps every base class to the resolver that inherits its handlers. First registration wins. */
collectExtendedResolversMap(resolvers) {
for (const resolver of resolvers) {
let baseClass = Object.getPrototypeOf(resolver.target);
while (baseClass.prototype) {
const claimedBy = this.resolverByBaseClass.get(baseClass);
if (claimedBy) {
this.warnOnAmbiguousBaseClass(baseClass, claimedBy, resolver);
}
else {
this.resolverByBaseClass.set(baseClass, resolver);
}
baseClass = Object.getPrototypeOf(baseClass);
}
}
}
/**
* Reached only when a base class is claimed twice. When both resolvers sit on one inheritance
* chain that is expected, and following the chain attributes the handlers correctly. When they
* sit on different branches only one can inherit the base, so the other silently misses out -
* worth reporting, but only if the base declares anything to miss out on.
*/
warnOnAmbiguousBaseClass(baseClass, claimedBy, resolver) {
if (this.sharesInheritanceChain(claimedBy.target, resolver.target) ||
!this.declaresHandler(baseClass)) {
return;
}
this.logger.warn(`${baseClass.name} is extended by both ${claimedBy.target.name} and ` +
`${resolver.target.name}, but its handlers can only be served by one class. They will be ` +
`resolved by ${claimedBy.target.name} because it was registered first, so ` +
`${resolver.target.name} will never serve them, and reordering imports can silently swap ` +
`which class wins. Detecting this also adds work to every schema build; declare these ` +
`handlers on a base class that only one resolver extends.`);
}
/** True when the class declares at least one handler, i.e. there is something to miss out on. */
declaresHandler(target) {
return this.handlerGroups.some((handlers) => handlers.some((handler) => handler.target === target));
}
/** True when either class derives from the other, i.e. they sit on a single inheritance chain. */
sharesInheritanceChain(a, b) {
return this.isDerivedFrom(a, b) || this.isDerivedFrom(b, a);
}
isDerivedFrom(derived, base) {
let current = Object.getPrototypeOf(derived);
while (current?.prototype) {
if (current === base) {
return true;
}
current = Object.getPrototypeOf(current);
}
return false;
}
}
exports.ResolverImplementationMap = ResolverImplementationMap;