UNPKG

@graphql-tools/import

Version:

A set of utils for faster development of GraphQL tools

140 lines (139 loc) 5.9 kB
import { Kind } from 'graphql'; import { FederatedLinkImport } from './link-import.js'; import { FederatedLinkUrl } from './link-url.js'; /** * Supports federation 1 */ function linkFromCoreArgs(args) { const feature = args.find(({ name, value }) => name.value === 'feature' && value.kind === Kind.STRING); if (feature) { const url = FederatedLinkUrl.fromUrl(feature.value.value); return new FederatedLink(url, null, []); } } function linkFromArgs(args) { let url; let imports = []; let as = null; for (const arg of args) { switch (arg.name.value) { case 'url': { if (arg.value.kind === Kind.STRING) { url = FederatedLinkUrl.fromUrl(arg.value.value); } else { console.warn(`Unexpected kind, ${arg.value.kind}, for argument "url" in @link.`); } break; } case 'import': { imports = FederatedLinkImport.fromTypedefs(arg.value); break; } case 'as': { if (arg.value.kind === Kind.STRING) { as = arg.value.value ?? null; } else { console.warn(`Unexpected kind, ${arg.value.kind}, for argument "as" in @link.`); } break; } default: { // ignore. Federation should validate links. } } } if (url !== undefined) { return new FederatedLink(url, as, imports); } } function namespaced(namespace, name) { if (namespace?.length) { if (name.startsWith('@')) { return `@${namespace}__${name.substring(1)}`; } return `${namespace}__${name}`; } return name; } export class FederatedLink { _url; _as; _imports; constructor(_url, _as, _imports) { this._url = _url; this._as = _as; this._imports = _imports; } /** Collects all `@link`s defined in graphql typedefs */ static fromTypedefs(typeDefs) { let links = []; for (const definition of typeDefs.definitions) { if (definition.kind === Kind.SCHEMA_EXTENSION || definition.kind === Kind.SCHEMA_DEFINITION) { const defLinks = definition.directives?.filter(directive => directive.name.value === 'link'); const parsedLinks = defLinks?.map(l => linkFromArgs(l.arguments ?? [])).filter(l => l !== undefined) ?? []; links = links.concat(parsedLinks); // Federation 1 support... Federation 1 uses "@core" instead of "@link", but behavior is similar enough that // it can be translated. const defCores = definition.directives?.filter(({ name }) => name.value === 'core'); const coreLinks = defCores ?.map(c => linkFromCoreArgs(c.arguments ?? [])) .filter(l => l !== undefined); if (coreLinks) { links = links.concat(...coreLinks); } } } return links; } /** * By default, `@link` will assign a prefix based on the name extracted from the URL. * If no name is present, a prefix will not be assigned. * See: https://specs.apollo.dev/link/v1.0/#@link.as */ get namespace() { return this._as ?? this._url.name; } toString() { return `@link(url: "${this._url}"${this._as ? `, as: "${this._as}"` : ''}${this._imports.length ? `, import: [${this._imports.join(', ')}]` : ''})`; } get defaultImport() { return this.namespace && `@${this.namespace}`; } /** The Link's identity. This is the unique identifier of a `@link`. */ get identity() { return this._url.identity; } supports(...args) { /** @ts-expect-error: ignore tuple error. These are tuples and can be spread. tsc is wrong. */ return this._url.supports(...args); } /** * Given the name of an element in a linked schema, this returns the name of that element * as it has been imported. This accounts for aliasing and namespacing unreferenced imports. * This can be used within implementations to reference the translated names of elements. * * The directive `@` prefix is removed from the returned name. This is to make it easier to match node names when visiting a schema definition. * When visiting nodes, a directive's name doesn't include the `@`. * However, the `@` is necessary for the input parameter in order to know how to correctly resolve the name. * Otherwise, setting the import argument as: import: ["foo"] would incorrectly return `foo` for `@foo`, when it should be `{namespace}__foo`. * * @name string The element name in the linked schema. If this is the name of the link (e.g. "example" when linking "https://foo.graphql-hive.com/example"), then this returns the default link import. * @returns The name of the element as it has been imported. */ resolveImportName(elementName) { if (this._url.name && elementName === `@${this._url.name}`) { // @note: default is a directive... So remove the `@` return this.defaultImport.substring(1); } const imported = this._imports.find(i => i.name === elementName); const resolvedName = imported?.as ?? imported?.name ?? namespaced(this.namespace, elementName); // Strip the `@` prefix for directives because in all implementations of mapping or visiting a schema, // directive names are not prefixed with `@`. The `@` is only for SDL. return resolvedName.startsWith('@') ? resolvedName.substring(1) : resolvedName; } get imports() { return this._imports; } }