@graphql-codegen/visitor-plugin-common
Version:
670 lines (669 loc) • 38 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.SelectionSetToObject = void 0;
const tslib_1 = require("tslib");
const crypto_1 = require("crypto");
const plugin_helpers_1 = require("@graphql-codegen/plugin-helpers");
const utils_1 = require("@graphql-tools/utils");
const auto_bind_1 = tslib_1.__importDefault(require("auto-bind"));
const graphql_1 = require("graphql");
const utils_js_1 = require("./utils.js");
const operationTypes = ['Query', 'Mutation', 'Subscription'];
function isMetadataFieldName(name) {
return ['__schema', '__type'].includes(name);
}
const metadataFieldMap = {
__schema: graphql_1.SchemaMetaFieldDef,
__type: graphql_1.TypeMetaFieldDef,
};
class SelectionSetToObject {
constructor(_processor, _scalars, _schema, _convertName, _getFragmentSuffix, _loadedFragments, _config, _parentSchemaType, _selectionSet) {
this._processor = _processor;
this._scalars = _scalars;
this._schema = _schema;
this._convertName = _convertName;
this._getFragmentSuffix = _getFragmentSuffix;
this._loadedFragments = _loadedFragments;
this._config = _config;
this._parentSchemaType = _parentSchemaType;
this._selectionSet = _selectionSet;
this._primitiveFields = [];
this._primitiveAliasedFields = [];
this._linksFields = [];
this._queriedForTypename = false;
(0, auto_bind_1.default)(this);
}
createNext(parentSchemaType, selectionSet) {
return new SelectionSetToObject(this._processor, this._scalars, this._schema, this._convertName.bind(this), this._getFragmentSuffix.bind(this), this._loadedFragments, this._config, parentSchemaType, selectionSet);
}
/**
* traverse the inline fragment nodes recursively for collecting the selectionSets on each type
*/
_collectInlineFragments(parentType, nodes, types) {
if ((0, graphql_1.isListType)(parentType) || (0, graphql_1.isNonNullType)(parentType)) {
return this._collectInlineFragments(parentType.ofType, nodes, types);
}
if ((0, graphql_1.isObjectType)(parentType)) {
for (const node of nodes) {
const typeOnSchema = node.typeCondition ? this._schema.getType(node.typeCondition.name.value) : parentType;
const { fields, inlines, spreads } = (0, utils_js_1.separateSelectionSet)(node.selectionSet.selections);
const spreadsUsage = this.buildFragmentSpreadsUsage(spreads);
const directives = node.directives || undefined;
// When we collect the selection sets of inline fragments we need to
// make sure directives on the inline fragments are stored in a way
// that can be associated back to the fields in the fragment, to
// support things like making those fields optional when deferring a
// fragment (using @defer).
const fieldsWithFragmentDirectives = fields.map(field => ({
...field,
fragmentDirectives: field.fragmentDirectives || directives,
}));
if ((0, graphql_1.isObjectType)(typeOnSchema)) {
this._appendToTypeMap(types, typeOnSchema.name, fieldsWithFragmentDirectives);
this._appendToTypeMap(types, typeOnSchema.name, spreadsUsage[typeOnSchema.name]);
this._appendToTypeMap(types, typeOnSchema.name, directives);
this._collectInlineFragments(typeOnSchema, inlines, types);
}
else if ((0, graphql_1.isInterfaceType)(typeOnSchema) && parentType.getInterfaces().includes(typeOnSchema)) {
this._appendToTypeMap(types, parentType.name, fields);
this._appendToTypeMap(types, parentType.name, spreadsUsage[parentType.name]);
this._collectInlineFragments(typeOnSchema, inlines, types);
}
}
}
else if ((0, graphql_1.isInterfaceType)(parentType)) {
const possibleTypes = (0, utils_js_1.getPossibleTypes)(this._schema, parentType);
for (const node of nodes) {
const schemaType = node.typeCondition ? this._schema.getType(node.typeCondition.name.value) : parentType;
const { fields, inlines, spreads } = (0, utils_js_1.separateSelectionSet)(node.selectionSet.selections);
const spreadsUsage = this.buildFragmentSpreadsUsage(spreads);
if ((0, graphql_1.isObjectType)(schemaType) && possibleTypes.find(possibleType => possibleType.name === schemaType.name)) {
this._appendToTypeMap(types, schemaType.name, fields);
this._appendToTypeMap(types, schemaType.name, spreadsUsage[schemaType.name]);
this._collectInlineFragments(schemaType, inlines, types);
}
else if ((0, graphql_1.isInterfaceType)(schemaType) && schemaType.name === parentType.name) {
for (const possibleType of possibleTypes) {
this._appendToTypeMap(types, possibleType.name, fields);
this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]);
this._collectInlineFragments(schemaType, inlines, types);
}
}
else {
// it must be an interface type that is spread on an interface field
for (const possibleType of possibleTypes) {
if (!node.typeCondition) {
throw new Error('Invalid state. Expected type condition for interface spread on a interface field.');
}
const fragmentSpreadType = this._schema.getType(node.typeCondition.name.value);
// the field should only be added to the valid selections
// in case the possible type actually implements the given interface
if ((0, graphql_1.isTypeSubTypeOf)(this._schema, possibleType, fragmentSpreadType)) {
this._appendToTypeMap(types, possibleType.name, fields);
this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]);
}
}
}
}
}
else if ((0, graphql_1.isUnionType)(parentType)) {
const possibleTypes = parentType.getTypes();
for (const node of nodes) {
const schemaType = node.typeCondition ? this._schema.getType(node.typeCondition.name.value) : parentType;
const { fields, inlines, spreads } = (0, utils_js_1.separateSelectionSet)(node.selectionSet.selections);
const spreadsUsage = this.buildFragmentSpreadsUsage(spreads);
if ((0, graphql_1.isObjectType)(schemaType) && possibleTypes.find(possibleType => possibleType.name === schemaType.name)) {
this._appendToTypeMap(types, schemaType.name, fields);
this._appendToTypeMap(types, schemaType.name, spreadsUsage[schemaType.name]);
this._collectInlineFragments(schemaType, inlines, types);
}
else if ((0, graphql_1.isInterfaceType)(schemaType)) {
const possibleInterfaceTypes = (0, utils_js_1.getPossibleTypes)(this._schema, schemaType);
for (const possibleType of possibleTypes) {
if (possibleInterfaceTypes.find(possibleInterfaceType => possibleInterfaceType.name === possibleType.name)) {
this._appendToTypeMap(types, possibleType.name, fields);
this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]);
this._collectInlineFragments(schemaType, inlines, types);
}
}
}
else {
for (const possibleType of possibleTypes) {
this._appendToTypeMap(types, possibleType.name, fields);
this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]);
}
}
}
}
}
_createInlineFragmentForFieldNodes(parentType, fieldNodes) {
return {
kind: graphql_1.Kind.INLINE_FRAGMENT,
typeCondition: {
kind: graphql_1.Kind.NAMED_TYPE,
name: {
kind: graphql_1.Kind.NAME,
value: parentType.name,
},
},
directives: [],
selectionSet: {
kind: graphql_1.Kind.SELECTION_SET,
selections: fieldNodes,
},
};
}
/**
* The `buildFragmentSpreadsUsage` method is used to collect fields from fragment spreads in the selection set.
* It creates a record of fragment spread usages, which includes the fragment name, type name, and the selection nodes
* inside the fragment.
*/
buildFragmentSpreadsUsage(spreads) {
const selectionNodesByTypeName = {};
for (const spread of spreads) {
const fragmentSpreadObject = this._loadedFragments.find(lf => lf.name === spread.name.value);
if (fragmentSpreadObject) {
const schemaType = this._schema.getType(fragmentSpreadObject.onType);
const possibleTypesForFragment = (0, utils_js_1.getPossibleTypes)(this._schema, schemaType);
for (const possibleType of possibleTypesForFragment) {
const fragmentSuffix = this._getFragmentSuffix(spread.name.value);
const usage = this.buildFragmentTypeName(spread.name.value, fragmentSuffix, possibleTypesForFragment.length === 1 ? null : possibleType.name);
selectionNodesByTypeName[possibleType.name] ||= [];
selectionNodesByTypeName[possibleType.name].push({
fragmentName: spread.name.value,
typeName: usage,
onType: fragmentSpreadObject.onType,
selectionNodes: [...fragmentSpreadObject.node.selectionSet.selections],
fragmentDirectives: [...spread.directives],
});
}
}
}
return selectionNodesByTypeName;
}
flattenSelectionSet(selections, parentSchemaType) {
const selectionNodesByTypeName = new Map();
const inlineFragmentSelections = [];
const fieldNodes = [];
const fragmentSpreads = [];
for (const selection of selections) {
switch (selection.kind) {
case graphql_1.Kind.FIELD:
fieldNodes.push(selection);
break;
case graphql_1.Kind.INLINE_FRAGMENT:
inlineFragmentSelections.push(selection);
break;
case graphql_1.Kind.FRAGMENT_SPREAD:
fragmentSpreads.push(selection);
break;
}
}
if (fieldNodes.length) {
inlineFragmentSelections.push(this._createInlineFragmentForFieldNodes(parentSchemaType ?? this._parentSchemaType, fieldNodes));
}
this._collectInlineFragments(parentSchemaType ?? this._parentSchemaType, inlineFragmentSelections, selectionNodesByTypeName);
const fragmentsUsage = this.buildFragmentSpreadsUsage(fragmentSpreads);
for (const [typeName, records] of Object.entries(fragmentsUsage)) {
this._appendToTypeMap(selectionNodesByTypeName, typeName, records);
}
return selectionNodesByTypeName;
}
_appendToTypeMap(types, typeName, nodes) {
if (!types.has(typeName)) {
types.set(typeName, []);
}
if (nodes && nodes.length > 0) {
types.get(typeName).push(...nodes);
}
}
_buildGroupedSelections(parentName) {
if (!this._selectionSet?.selections || this._selectionSet.selections.length === 0) {
return { grouped: {}, mustAddEmptyObject: true, dependentTypes: [] };
}
const selectionNodesByTypeName = this.flattenSelectionSet(this._selectionSet.selections);
// in case there is not a selection for each type, we need to add a empty type.
let mustAddEmptyObject = false;
const possibleTypes = (0, utils_js_1.getPossibleTypes)(this._schema, this._parentSchemaType);
const dependentTypes = [];
if (!this._config.mergeFragmentTypes || this._config.inlineFragmentTypes === 'mask') {
const grouped = possibleTypes.reduce((prev, type) => {
const typeName = type.name;
const schemaType = this._schema.getType(typeName);
if (!(0, graphql_1.isObjectType)(schemaType)) {
throw new TypeError(`Invalid state! Schema type ${typeName} is not a valid GraphQL object!`);
}
const allNodes = selectionNodesByTypeName.get(typeName) || [];
prev[typeName] ||= [];
// incrementalNodes are the ones flagged with @defer, meaning they become nullable
const { incrementalNodes, selectionNodes, fragmentSpreads } = allNodes.reduce((acc, node) => {
if ('fragmentDirectives' in node && (0, utils_js_1.hasIncrementalDeliveryDirectives)(node.fragmentDirectives)) {
acc.incrementalNodes.push(node);
}
else {
acc.selectionNodes.push(node);
}
return acc;
}, { selectionNodes: [], incrementalNodes: [], fragmentSpreads: [] });
const { fields, dependentTypes: subDependentTypes } = this.buildSelectionSet(schemaType, selectionNodes, {
parentFieldName: this.buildParentFieldName(typeName, parentName),
});
const transformedSet = this.selectionSetStringFromFields(fields);
if (transformedSet) {
prev[typeName].push(transformedSet);
}
dependentTypes.push(...subDependentTypes);
if (!transformedSet && !fragmentSpreads.length) {
mustAddEmptyObject = true;
}
for (const incrementalNode of incrementalNodes) {
if (this._config.inlineFragmentTypes === 'mask' && 'fragmentName' in incrementalNode) {
const { fields: incrementalFields, dependentTypes: incrementalDependentTypes } = this.buildSelectionSet(schemaType, [incrementalNode], { unsetTypes: true, parentFieldName: parentName });
const incrementalSet = this.selectionSetStringFromFields(incrementalFields);
prev[typeName].push(incrementalSet);
dependentTypes.push(...incrementalDependentTypes);
continue;
}
const { fields: initialFields, dependentTypes: initialDependentTypes } = this.buildSelectionSet(schemaType, [incrementalNode], { parentFieldName: parentName });
const { fields: subsequentFields, dependentTypes: subsequentDependentTypes } = this.buildSelectionSet(schemaType, [incrementalNode], { unsetTypes: true, parentFieldName: parentName });
const initialSet = this.selectionSetStringFromFields(initialFields);
const subsequentSet = this.selectionSetStringFromFields(subsequentFields);
dependentTypes.push(...initialDependentTypes, ...subsequentDependentTypes);
prev[typeName].push({ union: [initialSet, subsequentSet] });
}
return prev;
}, {});
return { grouped, mustAddEmptyObject, dependentTypes };
}
// Accumulate a map of selected fields to the typenames that
// share the exact same selected fields. When we find multiple
// typenames with the same set of fields, we can collapse the
// generated type to the selected fields and a string literal
// union of the typenames.
//
// E.g. {
// __typename: "foo" | "bar";
// shared: string;
// }
const grouped = possibleTypes.reduce((prev, type) => {
const typeName = type.name;
const schemaType = this._schema.getType(typeName);
if (!(0, graphql_1.isObjectType)(schemaType)) {
throw new TypeError(`Invalid state! Schema type ${typeName} is not a valid GraphQL object!`);
}
const selectionNodes = selectionNodesByTypeName.get(typeName) || [];
const { typeInfo, fields, dependentTypes: subDependentTypes, } = this.buildSelectionSet(schemaType, selectionNodes, {
parentFieldName: this.buildParentFieldName(typeName, parentName),
});
dependentTypes.push(...subDependentTypes);
const key = this.selectionSetStringFromFields(fields);
prev[key] = {
fields,
types: [...(prev[key]?.types ?? []), typeInfo || { name: '', type: type.name }].filter(Boolean),
};
return prev;
}, {});
// For every distinct set of fields, create the corresponding
// string literal union of typenames.
const compacted = Object.keys(grouped).reduce((acc, key) => {
const typeNames = grouped[key].types.map(t => t.type);
// Don't create very large string literal unions. TypeScript
// will stop comparing some nested union types types when
// they contain props with more than some number of string
// literal union members (testing with TS 4.5 stops working
// at 25 for a naive test case:
// https://www.typescriptlang.org/play?ts=4.5.4&ssl=29&ssc=10&pln=29&pc=1#code/C4TwDgpgBAKg9nAMgQwE4HNoF4BQV9QA+UA3ngRQJYB21EqAXDsQEQCMLzULATJ6wGZ+3ACzCWAVnEA2cQHZxADnEBOcWwAM6jl3Z9dbIQbEGpB2QYUHlBtbp5b7O1j30ujLky7Os4wABb0nAC+ODigkFAAQlBYUOT4xGQUVLT0TKzO3G7cHqLiPtwWrFasNqx2mY6ZWXrqeexe3GyF7MXNpc3lzZXZ1dm1ruI8DTxNvGahFEkJKTR0jLMpRNx+gaicy6E4APQ7AALAAM4AtJTo1HCoEDgANhDAUMgMsAgoGNikwQDcdw9QACMXjE4shfmEItAAGI0bCzGbLfDzdIGYbiBrjVrtFidFjdFi9dj9di1Ng5dgNNjjFrqbFsXFsfFsQkOYaDckjYbjNZBHDbPaHU7nS7XP6PZBsF4wuixL6-e6PAGS6KyiXfIA
const max_types = 20;
for (let i = 0; i < typeNames.length; i += max_types) {
const selectedTypes = typeNames.slice(i, i + max_types);
const typenameUnion = grouped[key].types[0].name
? this._processor.transformTypenameField(selectedTypes.join(' | '), grouped[key].types[0].name)
: [];
const transformedSet = this.selectionSetStringFromFields([...typenameUnion, ...grouped[key].fields]);
// The keys here will be used to generate intermediary
// fragment names. To avoid blowing up the type name on large
// unions, calculate a stable hash here instead.
//
// Also use fragment hashing if skipTypename is true, since we
// then don't have a typename for naming the fragment.
acc[selectedTypes.length <= 3
? // Remove quote marks to produce a valid type name
selectedTypes.map(t => t.replace(/'/g, '')).join('_')
: (0, crypto_1.createHash)('sha256')
.update(selectedTypes.join() || transformedSet || '')
// Remove invalid characters to produce a valid type name
.digest('base64')
.replace(/[=+/]/g, '')] = [transformedSet];
}
return acc;
}, {});
return { grouped: compacted, mustAddEmptyObject, dependentTypes };
}
selectionSetStringFromFields(fields) {
const allStrings = fields.filter((f) => typeof f === 'string');
const allObjects = fields
.filter((f) => typeof f !== 'string')
.map(t => `${t.name}: ${t.type}`);
const mergedObjects = allObjects.length ? this._processor.buildFieldsIntoObject(allObjects) : null;
const transformedSet = this._processor.buildSelectionSetFromStrings([...allStrings, mergedObjects].filter(Boolean));
return transformedSet;
}
buildSelectionSet(parentSchemaType, selectionNodes, options) {
const primitiveFields = new Map();
const primitiveAliasFields = new Map();
const linkFieldSelectionSets = new Map();
let requireTypename = false;
// usages via fragment typescript type
const fragmentsSpreadUsages = [];
// ensure we mutate no function params
selectionNodes = [...selectionNodes];
let inlineFragmentConditional = false;
for (const selectionNode of selectionNodes) {
if ('kind' in selectionNode) {
if (selectionNode.kind === 'Field') {
if (selectionNode.selectionSet) {
let selectedField = null;
const fields = parentSchemaType.getFields();
selectedField = fields[selectionNode.name.value];
if (isMetadataFieldName(selectionNode.name.value)) {
selectedField = metadataFieldMap[selectionNode.name.value];
}
if (!selectedField) {
continue;
}
const fieldName = (0, utils_js_1.getFieldNodeNameValue)(selectionNode);
let linkFieldNode = linkFieldSelectionSets.get(fieldName);
if (linkFieldNode) {
linkFieldNode = {
...linkFieldNode,
field: {
...linkFieldNode.field,
selectionSet: (0, utils_js_1.mergeSelectionSets)(linkFieldNode.field.selectionSet, selectionNode.selectionSet),
},
};
}
else {
linkFieldNode = {
selectedFieldType: selectedField.type,
field: selectionNode,
};
}
linkFieldSelectionSets.set(fieldName, linkFieldNode);
}
else if (selectionNode.alias) {
primitiveAliasFields.set(selectionNode.alias.value, selectionNode);
}
else if (selectionNode.name.value === '__typename') {
requireTypename = true;
}
else {
primitiveFields.set(selectionNode.name.value, selectionNode);
}
}
else if (selectionNode.kind === 'Directive') {
if (['skip', 'include'].includes(selectionNode?.name?.value)) {
inlineFragmentConditional = true;
}
}
else {
throw new TypeError('Unexpected type.');
}
continue;
}
if (this._config.inlineFragmentTypes === 'combine' || this._config.inlineFragmentTypes === 'mask') {
fragmentsSpreadUsages.push(selectionNode.typeName);
const isApolloUnmaskEnabled = this._config.customDirectives.apolloUnmask;
if (!isApolloUnmaskEnabled ||
(isApolloUnmaskEnabled && !selectionNode.fragmentDirectives?.some(d => d.name.value === 'unmask'))) {
continue;
}
}
// Handle Fragment Spreads by generating inline types.
const fragmentType = this._schema.getType(selectionNode.onType);
if (fragmentType == null) {
throw new TypeError(`Unexpected error: Type ${selectionNode.onType} does not exist within schema.`);
}
if (parentSchemaType.name === selectionNode.onType ||
parentSchemaType.getInterfaces().find(iinterface => iinterface.name === selectionNode.onType) != null ||
((0, graphql_1.isUnionType)(fragmentType) &&
fragmentType.getTypes().find(objectType => objectType.name === parentSchemaType.name))) {
// also process fields from fragment that apply for this parentType
const flatten = this.flattenSelectionSet(selectionNode.selectionNodes, parentSchemaType);
const typeNodes = flatten.get(parentSchemaType.name) ?? [];
selectionNodes.push(...typeNodes);
for (const iinterface of parentSchemaType.getInterfaces()) {
const typeNodes = flatten.get(iinterface.name) ?? [];
selectionNodes.push(...typeNodes);
}
}
}
const linkFields = [];
const linkFieldsInterfaces = [];
for (const { field, selectedFieldType } of linkFieldSelectionSets.values()) {
const realSelectedFieldType = (0, plugin_helpers_1.getBaseType)(selectedFieldType);
const selectionSet = this.createNext(realSelectedFieldType, field.selectionSet);
const fieldName = field.alias?.value ?? field.name.value;
const selectionSetObjects = selectionSet.transformSelectionSet(options.parentFieldName ? `${options.parentFieldName}_${fieldName}` : fieldName);
linkFieldsInterfaces.push(...selectionSetObjects.dependentTypes);
const isConditional = (0, utils_js_1.hasConditionalDirectives)(field) || inlineFragmentConditional;
const isOptional = options.unsetTypes;
linkFields.push({
alias: field.alias
? this._processor.config.formatNamedField(field.alias.value, selectedFieldType, isConditional, isOptional)
: undefined,
name: this._processor.config.formatNamedField(field.name.value, selectedFieldType, isConditional, isOptional),
type: realSelectedFieldType.name,
selectionSet: this._processor.config.wrapTypeWithModifiers(selectionSetObjects.mergedTypeString.split(`\n`).join(`\n `), selectedFieldType),
});
}
const typeInfoField = this.buildTypeNameField(parentSchemaType, this._config.nonOptionalTypename, this._config.addTypename, requireTypename, this._config.skipTypeNameForRoot);
const transformed = [
// Only add the typename field if we're not merging fragment
// types. If we are merging, we need to wait until we know all
// the involved typenames.
...(typeInfoField && (!this._config.mergeFragmentTypes || this._config.inlineFragmentTypes === 'mask')
? this._processor.transformTypenameField(typeInfoField.type, typeInfoField.name)
: []),
...this._processor.transformPrimitiveFields(parentSchemaType, Array.from(primitiveFields.values()).map(field => ({
isConditional: (0, utils_js_1.hasConditionalDirectives)(field),
fieldName: field.name.value,
})), options.unsetTypes),
...this._processor.transformAliasesPrimitiveFields(parentSchemaType, Array.from(primitiveAliasFields.values()).map(field => ({
alias: field.alias.value,
fieldName: field.name.value,
isConditional: (0, utils_js_1.hasConditionalDirectives)(field),
})), options.unsetTypes),
...this._processor.transformLinkFields(linkFields, options.unsetTypes),
].filter(Boolean);
const allStrings = transformed.filter(t => typeof t === 'string');
const allObjectsMerged = transformed
.filter(t => typeof t !== 'string')
.map((t) => `${t.name}: ${t.type}`);
let mergedObjectsAsString = null;
if (allObjectsMerged.length > 0) {
mergedObjectsAsString = this._processor.buildFieldsIntoObject(allObjectsMerged);
}
const fields = [...allStrings, mergedObjectsAsString].filter(Boolean);
if (fragmentsSpreadUsages.length) {
if (this._config.inlineFragmentTypes === 'combine') {
fields.push(...fragmentsSpreadUsages);
}
else if (this._config.inlineFragmentTypes === 'mask') {
fields.push(`{ ' $fragmentRefs'?: { ${fragmentsSpreadUsages
.map(name => `'${name}': ${options.unsetTypes ? `Incremental<${name}>` : name}`)
.join(`;`)} } }`);
}
}
return { typeInfo: typeInfoField, fields, dependentTypes: linkFieldsInterfaces };
}
buildTypeNameField(type, nonOptionalTypename = this._config.nonOptionalTypename, addTypename = this._config.addTypename, queriedForTypename = this._queriedForTypename, skipTypeNameForRoot = this._config.skipTypeNameForRoot) {
const rootTypes = (0, utils_1.getRootTypes)(this._schema);
if (rootTypes.has(type) && skipTypeNameForRoot && !queriedForTypename) {
return null;
}
if (nonOptionalTypename || addTypename || queriedForTypename) {
const optionalTypename = !queriedForTypename && !nonOptionalTypename;
return {
name: `${this._processor.config.formatNamedField('__typename')}${optionalTypename ? '?' : ''}`,
type: `'${type.name}'`,
};
}
return null;
}
getUnknownType() {
return 'never';
}
getEmptyObjectType() {
return `{}`;
}
getEmptyObjectTypeString(mustAddEmptyObject) {
return mustAddEmptyObject ? this.getEmptyObjectType() : ``;
}
transformSelectionSet(fieldName) {
const possibleTypesList = (0, utils_js_1.getPossibleTypes)(this._schema, this._parentSchemaType);
const possibleTypes = possibleTypesList.map(v => v.name).sort();
const fieldSelections = [
...(0, utils_js_1.getFieldNames)({ selections: this._selectionSet.selections, loadedFragments: this._loadedFragments }),
].sort();
// Optimization: Do not create new dependentTypes if fragment typename exists in cache
// 2-layer cache: LOC => Field Selection Type Combination => cachedTypeString
const objMap = this._processor.typeCache.get(this._selectionSet.loc) ?? new Map();
this._processor.typeCache.set(this._selectionSet.loc, objMap);
const cacheHashKey = `${fieldSelections.join(',')} @ ${possibleTypes.join('|')}`;
const [cachedTypeString] = objMap.get(cacheHashKey) ?? [];
if (cachedTypeString) {
// reuse previously generated type, as it is identical
return {
mergedTypeString: cachedTypeString,
// there are no new dependent types, as this is a nth use of the same type
dependentTypes: [],
};
}
const result = this.transformSelectionSetUncached(fieldName);
objMap.set(cacheHashKey, [result.mergedTypeString, fieldName]);
if (this._selectionSet.loc) {
this._processor.typeCache.set(this._selectionSet.loc, objMap);
}
return result;
}
transformSelectionSetUncached(fieldName) {
const { grouped, mustAddEmptyObject, dependentTypes: subDependentTypes } = this._buildGroupedSelections(fieldName);
// This might happen in case we have an interface, that is being queries, without any GraphQL
// "type" that implements it. It will lead to a runtime error, but we aim to try to reflect that in
// build time as well.
if (Object.keys(grouped).length === 0) {
return {
mergedTypeString: this.getUnknownType(),
dependentTypes: subDependentTypes,
};
}
const dependentTypes = Object.keys(grouped)
.map(typeName => {
const relevant = grouped[typeName].filter(Boolean);
return relevant.map(objDefinition => {
const name = fieldName ? `${fieldName}_${typeName}` : typeName;
return {
name,
content: typeof objDefinition === 'string' ? objDefinition : objDefinition.union.join(' | '),
isUnionType: !!(typeof objDefinition !== 'string' && objDefinition.union.length > 1),
};
});
})
.filter(pairs => pairs.length > 0);
const typeParts = [
...dependentTypes.map(pair => pair
.map(({ name, content, isUnionType }) =>
// unions need to be wrapped, as intersections have higher precedence
this._config.extractAllFieldsToTypes ? name : isUnionType ? `(${content})` : content)
.join(' & ')),
this.getEmptyObjectTypeString(mustAddEmptyObject),
].filter(Boolean);
const content = typeParts.join(' | ');
if (typeParts.length > 1 && this._config.extractAllFieldsToTypes) {
return {
mergedTypeString: fieldName,
dependentTypes: [
...subDependentTypes,
...dependentTypes.flat(1),
{ name: fieldName, content, isUnionType: true },
],
};
}
return {
mergedTypeString: content,
dependentTypes: [...subDependentTypes, ...dependentTypes.flat(1)],
isUnionType: typeParts.length > 1,
};
}
transformFragmentSelectionSetToTypes(fragmentName, fragmentSuffix, declarationBlockConfig) {
const mergedTypeString = this.buildFragmentTypeName(fragmentName, fragmentSuffix);
const { grouped, dependentTypes } = this._buildGroupedSelections(mergedTypeString);
const subTypes = Object.keys(grouped).flatMap(typeName => {
const possibleFields = grouped[typeName].filter(Boolean);
const declarationName = this.buildFragmentTypeName(fragmentName, fragmentSuffix, typeName);
if (possibleFields.length === 0) {
if (!this._config.addTypename) {
return [{ name: declarationName, content: this.getEmptyObjectType() }];
}
return [];
}
const flatFields = possibleFields.map(selectionObject => {
if (typeof selectionObject === 'string')
return { value: selectionObject };
return { value: selectionObject.union.join(' | '), isUnionType: true };
});
const content = flatFields.length > 1
? flatFields.map(({ value, isUnionType }) => (isUnionType ? `(${value})` : value)).join(' & ')
: flatFields.map(({ value }) => value).join(' & ');
return {
name: declarationName,
content,
isUnionType: false,
};
});
const fragmentMaskPartial = this._config.inlineFragmentTypes === 'mask' ? ` & { ' $fragmentName'?: '${mergedTypeString}' }` : '';
// TODO: unify with line 308 from base-documents-visitor
const dependentTypesContent = this._config.extractAllFieldsToTypes
? dependentTypes.map(i => new utils_js_1.DeclarationBlock(declarationBlockConfig)
.export(true)
.asKind('type')
.withName(i.name)
.withContent(i.content).string)
: [];
if (subTypes.length === 1) {
return [
...dependentTypesContent,
new utils_js_1.DeclarationBlock(declarationBlockConfig)
.export()
.asKind('type')
.withName(mergedTypeString)
.withContent(subTypes[0].content + fragmentMaskPartial).string,
].join('\n');
}
return [
...dependentTypesContent,
...subTypes.map(t => new utils_js_1.DeclarationBlock(declarationBlockConfig)
.export(this._config.exportFragmentSpreadSubTypes)
.asKind('type')
.withName(t.name)
.withContent(`${t.content}${this._config.inlineFragmentTypes === 'mask' ? ` & { ' $fragmentName'?: '${t.name}' }` : ''}`).string),
new utils_js_1.DeclarationBlock(declarationBlockConfig)
.export()
.asKind('type')
.withName(mergedTypeString)
.withContent(subTypes.map(t => t.name).join(' | ')).string,
].join('\n');
}
buildFragmentTypeName(name, suffix, typeName = '') {
return this._convertName(name, {
useTypesPrefix: true,
suffix: typeName && suffix ? `_${typeName}_${suffix}` : typeName ? `_${typeName}` : suffix,
});
}
buildParentFieldName(typeName, parentName) {
// queries/mutations/fragments are guaranteed to be unique type names,
// so we can skip affixing the field names with typeName
return operationTypes.includes(typeName) ? parentName : `${parentName}_${typeName}`;
}
}
exports.SelectionSetToObject = SelectionSetToObject;
;