@omnigraph/soap
Version:
934 lines • 70.7 kB
JavaScript
import { XMLParser } from 'fast-xml-parser';
import { DirectiveLocation, GraphQLBoolean, GraphQLDirective, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLString, } from 'graphql';
import { GraphQLJSON, SchemaComposer } from 'graphql-compose';
import { GraphQLBigInt, GraphQLByte, GraphQLDate, GraphQLDateTime, GraphQLDuration, GraphQLHexadecimal, GraphQLNegativeInt, GraphQLNonNegativeInt, GraphQLNonPositiveInt, GraphQLPositiveInt, GraphQLTime, GraphQLUnsignedInt, GraphQLURL, GraphQLVoid, RegularExpression, } from 'graphql-scalars';
import { path, process } from '@graphql-mesh/cross-helpers';
import { getInterpolatedHeadersFactory } from '@graphql-mesh/string-interpolation';
import { ObjMapScalar } from '@graphql-mesh/transport-common';
import { defaultImportFn, DefaultLogger, readFileOrUrl, sanitizeNameForGraphQL, } from '@graphql-mesh/utils';
import { fetch as defaultFetchFn } from '@whatwg-node/fetch';
import { PARSE_XML_OPTIONS } from './utils.js';
const SOAPHeadersInput = new GraphQLInputObjectType({
name: 'SOAPHeaders',
fields: {
namespace: {
type: GraphQLString,
},
alias: {
type: GraphQLString,
},
headers: {
type: ObjMapScalar,
},
},
});
const soapDirective = new GraphQLDirective({
name: 'soap',
locations: [DirectiveLocation.FIELD_DEFINITION],
args: {
elementName: {
type: GraphQLString,
},
bindingNamespace: {
type: GraphQLString,
},
endpoint: {
type: GraphQLString,
},
subgraph: {
type: GraphQLString,
},
bodyAlias: {
type: GraphQLString,
},
soapHeaders: {
type: SOAPHeadersInput,
},
soapAction: {
type: GraphQLString,
},
soapNamespace: {
type: GraphQLString,
},
headerArgNames: {
type: new GraphQLList(GraphQLString),
},
argNamespaces: {
type: ObjMapScalar,
},
typeNamespaces: {
type: ObjMapScalar,
},
},
});
const QUERY_PREFIXES = [
'get',
'find',
'list',
'search',
'count',
'exists',
'fetch',
'load',
'query',
'select',
];
function isQueryOperationName(operationName) {
return QUERY_PREFIXES.some(prefix => operationName.toLowerCase().startsWith(prefix));
}
// Matches any RFC-3986 scheme (2+ chars to avoid mistaking Windows drive
// letters like "C:" for URI schemes). The scheme isn't required to be
// followed by "//" — `file:/tmp/x`, `urn:isbn:1234`, etc. are valid
// absolute URIs.
const URI_SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+.-]+:/;
/**
* Resolve a (possibly relative) WSDL/XSD location against the location of the
* document doing the import. Mirrors XML Schema's xml:base resolution rules:
* relative `schemaLocation` / `wsdl:import location` is resolved against the
* importing document's own URI, not against a fixed root.
*
* Handles both URIs (http, https, file, urn, …) and filesystem paths uniformly.
*/
function resolveLocation(base, location) {
// Already-absolute URIs (any scheme) pass through.
if (URI_SCHEME_RE.test(location))
return location;
if (!base)
return location;
if (URI_SCHEME_RE.test(base)) {
// base is a URI — let URL handle "../" and root-relative ("/foo") paths.
return new URL(location, base).href;
}
// base is a filesystem path. A filesystem-absolute location passes through;
// anything else is resolved relative to base's containing directory.
if (path.isAbsolute(location))
return location;
return path.resolve(path.dirname(base), location);
}
/**
* Convert a namespace URI to a GraphQL-safe identifier slug.
* Uses a single linear pass to avoid polynomial regex backtracking on
* user-controlled input (CodeQL js/polynomial-redos).
*/
function namespaceToSlug(namespace) {
// Determine start index after optional protocol prefix
let start = 0;
if (namespace.startsWith('https://')) {
start = 8;
}
else if (namespace.startsWith('http://')) {
start = 7;
}
// Build slug in one pass:
// - keep alphanumeric characters as-is
// - collapse any run of non-alphanumeric characters into a single '_'
// - skip leading and trailing '_' (pendingUnderscore is only flushed when
// a subsequent alphanumeric character is found)
// Use an array of chunks to avoid O(n^2) repeated string concatenation.
const chunks = [];
let pendingUnderscore = false;
for (let i = start; i < namespace.length; i++) {
const code = namespace.charCodeAt(i);
if ((code >= 0x30 && code <= 0x39) || // 0-9
(code >= 0x41 && code <= 0x5a) || // A-Z
(code >= 0x61 && code <= 0x7a) // a-z
) {
if (pendingUnderscore && chunks.length > 0) {
chunks.push('_');
}
pendingUnderscore = false;
chunks.push(namespace[i]);
}
else {
pendingUnderscore = true;
}
}
// If nothing was accumulated (e.g. namespace is symbols-only or empty after
// stripping the protocol prefix), return a safe sentinel so callers always
// receive a non-empty, GraphQL-Name-compatible string.
if (chunks.length === 0) {
return '_';
}
// Prefix a leading digit so the result is a valid GraphQL identifier
const firstCode = chunks[0].charCodeAt(0);
if (firstCode >= 0x30 && firstCode <= 0x39) {
chunks.unshift('_');
}
return chunks.join('');
}
export class SOAPLoader {
constructor(options) {
this.schemaComposer = new SchemaComposer();
this.namespaceDefinitionsMap = new Map();
this.namespaceComplexTypesMap = new Map();
this.namespaceSimpleTypesMap = new Map();
this.namespacePortTypesMap = new Map();
this.namespaceBindingMap = new Map();
this.namespaceMessageMap = new Map();
this.aliasMap = new WeakMap();
this.messageOutputTCMap = new WeakMap();
this.complexTypeInputTCMap = new WeakMap();
this.complexTypeOutputTCMap = new WeakMap();
this.simpleTypeTCMap = new WeakMap();
this.namespaceTypePrefixMap = new Map();
/** Maps GraphQL input type name → the XSD namespace URI where that type is defined. */
this.typeNamespaceMap = new Map();
this.namespaceElementRefMap = new Map();
this.loadedLocations = new Map();
this.xmlParser = new XMLParser(PARSE_XML_OPTIONS);
this.fetchFn = options.fetch || defaultFetchFn;
this.logger = options.logger || new DefaultLogger(options.subgraphName);
this.subgraphName = options.subgraphName;
this.loadXMLSchemaNamespace();
this.schemaComposer.addDirective(soapDirective);
this.schemaHeadersFactory = getInterpolatedHeadersFactory(options.schemaHeaders || {});
this.endpoint = options.endpoint;
this.cwd = options.cwd;
this.soapHeaders = options.soapHeaders;
this.bodyAlias = options.bodyAlias;
this.soapNamespace = 'http://schemas.xmlsoap.org/soap/envelope/';
}
loadXMLSchemaNamespace() {
const namespace = 'http://www.w3.org/2001/XMLSchema';
const simpleTypeGraphQLScalarMap = new Map([
['anyType', GraphQLJSON],
['anySimpleType', GraphQLString],
['anyURI', GraphQLURL],
['base64Binary', GraphQLByte],
['byte', GraphQLByte],
['boolean', GraphQLBoolean],
['date', GraphQLDate],
['dateTime', GraphQLDateTime],
['decimal', GraphQLFloat],
['double', GraphQLFloat],
['duration', GraphQLDuration],
['float', GraphQLFloat],
['int', GraphQLInt],
['integer', GraphQLInt],
['negativeInteger', GraphQLNegativeInt],
['nonNegativeInteger', GraphQLNonNegativeInt],
['nonPositiveInteger', GraphQLNonPositiveInt],
['positiveInteger', GraphQLPositiveInt],
['hexBinary', GraphQLHexadecimal],
['long', GraphQLBigInt],
['gDay', GraphQLString],
['gMonth', GraphQLString],
['gMonthDay', GraphQLString],
['gYear', GraphQLString],
['gYearMonth', GraphQLString],
['NOTATION', GraphQLString],
['QName', GraphQLString],
['short', GraphQLInt],
['string', GraphQLString],
['unsignedByte', GraphQLByte],
['unsignedInt', GraphQLUnsignedInt],
['unsignedLong', GraphQLBigInt],
['unsignedShort', GraphQLUnsignedInt],
['time', GraphQLTime],
]);
const namespaceSimpleTypesMap = this.getNamespaceSimpleTypeMap(namespace);
for (const [singleTypeName, scalarType] of simpleTypeGraphQLScalarMap) {
const singleType = {
attributes: {
name: singleTypeName,
},
};
namespaceSimpleTypesMap.set(singleTypeName, singleType);
const simpleTypeTC = this.schemaComposer.createScalarTC(scalarType);
this.simpleTypeTCMap.set(singleType, simpleTypeTC);
}
}
getNamespaceDefinitions(namespace) {
let namespaceDefinitions = this.namespaceDefinitionsMap.get(namespace);
if (!namespaceDefinitions) {
namespaceDefinitions = [];
this.namespaceDefinitionsMap.set(namespace, namespaceDefinitions);
}
return namespaceDefinitions;
}
getNamespaceComplexTypeMap(namespace) {
let namespaceComplexTypes = this.namespaceComplexTypesMap.get(namespace);
if (!namespaceComplexTypes) {
namespaceComplexTypes = new Map();
this.namespaceComplexTypesMap.set(namespace, namespaceComplexTypes);
}
return namespaceComplexTypes;
}
getNamespaceSimpleTypeMap(namespace) {
let namespaceSimpleTypes = this.namespaceSimpleTypesMap.get(namespace);
if (!namespaceSimpleTypes) {
namespaceSimpleTypes = new Map();
this.namespaceSimpleTypesMap.set(namespace, namespaceSimpleTypes);
}
return namespaceSimpleTypes;
}
getNamespacePortTypeMap(namespace) {
let namespacePortTypes = this.namespacePortTypesMap.get(namespace);
if (!namespacePortTypes) {
namespacePortTypes = new Map();
this.namespacePortTypesMap.set(namespace, namespacePortTypes);
}
return namespacePortTypes;
}
getNamespaceBindingMap(namespace) {
let namespaceBindingMap = this.namespaceBindingMap.get(namespace);
if (!namespaceBindingMap) {
namespaceBindingMap = new Map();
this.namespaceBindingMap.set(namespace, namespaceBindingMap);
}
return namespaceBindingMap;
}
getNamespaceMessageMap(namespace) {
let namespaceMessageMap = this.namespaceMessageMap.get(namespace);
if (!namespaceMessageMap) {
namespaceMessageMap = new Map();
this.namespaceMessageMap.set(namespace, namespaceMessageMap);
}
return namespaceMessageMap;
}
async loadSchema(schemaObj, parentAliasMap = new Map(), baseUrl) {
// A bare <xsd:schema> wrapper that only contains imports (no targetNamespace,
// no type definitions) is a valid pattern — it lets a WSDL pull external
// schemas into its <wsdl:types> without redefining anything itself. Process
// its imports here. A "chameleon" schema (no targetNamespace + own type
// definitions) is a different, larger case the loader doesn't yet support;
// surface it loudly rather than silently dropping the definitions.
if (!schemaObj.attributes || !schemaObj.attributes.targetNamespace) {
if (schemaObj.import) {
for (const importObj of schemaObj.import) {
const importLocation = importObj.attributes.schemaLocation;
if (importLocation) {
await this.fetchXSD(importLocation, parentAliasMap, baseUrl);
}
}
}
if (schemaObj.complexType || schemaObj.simpleType || schemaObj.element) {
throw new Error('<xsd:schema> without targetNamespace but with type definitions ' +
'(chameleon schemas) is not yet supported by the SOAP loader.');
}
return;
}
const schemaNamespace = schemaObj.attributes.targetNamespace;
const aliasMap = this.getAliasMapFromAttributes(schemaObj.attributes);
let typePrefix = this.namespaceTypePrefixMap.get(schemaNamespace);
if (!typePrefix) {
typePrefix =
schemaObj.attributes.id ||
[...aliasMap.entries()].find(([, namespace]) => namespace === schemaNamespace)?.[0];
this.namespaceTypePrefixMap.set(schemaNamespace, typePrefix);
}
for (const [alias, namespace] of parentAliasMap) {
if (!aliasMap.has(alias)) {
aliasMap.set(alias, namespace);
}
}
if (schemaObj.import) {
for (const importObj of schemaObj.import) {
const importLocation = importObj.attributes.schemaLocation;
if (importLocation) {
await this.fetchXSD(importLocation, parentAliasMap, baseUrl);
}
}
}
// Complex and simple types can be inside element tag or outside of it
if (schemaObj.complexType) {
const namespaceComplexTypes = this.getNamespaceComplexTypeMap(schemaNamespace);
for (const complexType of schemaObj.complexType) {
namespaceComplexTypes.set(complexType.attributes.name, complexType);
this.aliasMap.set(complexType, aliasMap);
}
}
if (schemaObj.simpleType) {
const namespaceSimpleTypes = this.getNamespaceSimpleTypeMap(schemaNamespace);
for (const simpleType of schemaObj.simpleType) {
namespaceSimpleTypes.set(simpleType.attributes.name, simpleType);
this.aliasMap.set(simpleType, aliasMap);
}
}
if (schemaObj.element) {
for (const elementObj of schemaObj.element) {
if (elementObj.complexType) {
const namespaceComplexTypes = this.getNamespaceComplexTypeMap(schemaNamespace);
for (const complexType of elementObj.complexType) {
// Sometimes type name is defined on element object
complexType.attributes = complexType.attributes || {};
complexType.attributes.name = elementObj.attributes.name;
namespaceComplexTypes.set(complexType.attributes.name, complexType);
this.aliasMap.set(complexType, aliasMap);
}
}
if (elementObj.simpleType) {
const namespaceSimpleTypes = this.getNamespaceSimpleTypeMap(schemaNamespace);
for (const simpleType of elementObj.simpleType) {
simpleType.attributes = simpleType.attributes || {};
simpleType.attributes.name = elementObj.attributes.name;
namespaceSimpleTypes.set(simpleType.attributes.name, simpleType);
this.aliasMap.set(simpleType, aliasMap);
}
}
if (elementObj.attributes?.type) {
const [refTypeNamespaceAlias, refTypeName] = elementObj.attributes.type.split(':');
const refTypeNamespace = aliasMap.get(refTypeNamespaceAlias);
if (!refTypeNamespace) {
throw new Error(`Invalid namespace alias: ${refTypeNamespaceAlias}`);
}
const refComplexType = this.getNamespaceComplexTypeMap(refTypeNamespace).get(refTypeName);
if (refComplexType) {
this.getNamespaceComplexTypeMap(schemaNamespace).set(elementObj.attributes.name, refComplexType);
}
const refSimpleType = this.getNamespaceSimpleTypeMap(refTypeNamespace).get(refTypeName);
if (refSimpleType) {
this.getNamespaceSimpleTypeMap(schemaNamespace).set(elementObj.attributes.name, refSimpleType);
}
if (!refComplexType && !refSimpleType) {
// Forward reference: referenced type not yet loaded. Store for lazy lookup.
let elementRefs = this.namespaceElementRefMap.get(schemaNamespace);
if (!elementRefs) {
elementRefs = new Map();
this.namespaceElementRefMap.set(schemaNamespace, elementRefs);
}
elementRefs.set(elementObj.attributes.name, {
typeName: refTypeName,
typeNamespace: refTypeNamespace,
});
}
}
}
}
}
async loadDefinition(definition, baseUrl) {
this.getNamespaceDefinitions(definition.attributes.targetNamespace).push(definition);
if (definition.attributes.soap12) {
this.soapNamespace = 'http://www.w3.org/2003/05/soap-envelope';
}
const definitionAliasMap = this.getAliasMapFromAttributes(definition.attributes);
const definitionNamespace = definition.attributes.targetNamespace;
const typePrefix = definition.attributes.name ||
[...definitionAliasMap.entries()].find(([, namespace]) => namespace === definitionNamespace)?.[0] ||
'';
this.namespaceTypePrefixMap.set(definition.attributes.targetNamespace, typePrefix);
if (definition.import) {
for (const importObj of definition.import) {
const importLocation = importObj.attributes.location;
if (importLocation) {
await this.fetchWSDL(importLocation, baseUrl);
}
}
}
if (definition.types) {
for (const typesObj of definition.types) {
for (const schemaObj of typesObj.schema) {
await this.loadSchema(schemaObj, definitionAliasMap, baseUrl);
}
}
}
if (definition.portType) {
const namespacePortTypes = this.getNamespacePortTypeMap(definition.attributes.targetNamespace);
for (const portTypeObj of definition.portType) {
namespacePortTypes.set(portTypeObj.attributes.name, portTypeObj);
this.aliasMap.set(portTypeObj, definitionAliasMap);
}
}
if (definition.binding) {
const namespaceBindingMap = this.getNamespaceBindingMap(definition.attributes.targetNamespace);
for (const bindingObj of definition.binding) {
namespaceBindingMap.set(bindingObj.attributes.name, bindingObj);
this.aliasMap.set(bindingObj, definitionAliasMap);
}
}
if (definition.message) {
const namespaceMessageMap = this.getNamespaceMessageMap(definition.attributes.targetNamespace);
for (const messageObj of definition.message) {
namespaceMessageMap.set(messageObj.attributes.name, messageObj);
this.aliasMap.set(messageObj, definitionAliasMap);
}
}
const serviceAndPortAliasMap = this.getAliasMapFromAttributes(definition.attributes);
if (definition.service) {
for (const serviceObj of definition.service) {
const serviceName = serviceObj.attributes.name;
for (const portObj of serviceObj.port) {
const portName = portObj.attributes.name;
const [bindingNamespaceAlias, bindingName] = portObj.attributes.binding.split(':');
const bindingNamespace = serviceAndPortAliasMap.get(bindingNamespaceAlias);
if (!bindingNamespace) {
throw new Error(`Namespace alias: ${bindingNamespaceAlias} is undefined!`);
}
const bindingObj = this.getNamespaceBindingMap(bindingNamespace).get(bindingName);
if (!bindingObj) {
throw new Error(`Binding: ${bindingName} is not defined in ${bindingNamespace} needed for ${serviceName}->${portName}`);
}
const bindingAliasMap = this.aliasMap.get(bindingObj);
if (!bindingAliasMap) {
throw new Error(`Namespace alias definitions couldn't be found for ${bindingName}`);
}
const [portTypeNamespaceAlias, portTypeName] = bindingObj.attributes.type.split(':');
const portTypeNamespace = bindingAliasMap.get(portTypeNamespaceAlias);
if (!portTypeNamespace) {
throw new Error(`Namespace alias: ${portTypeNamespaceAlias} is undefined!`);
}
const portTypeObj = this.getNamespacePortTypeMap(portTypeNamespace).get(portTypeName);
if (!portTypeObj) {
throw new Error(`Port Type: ${portTypeName} is not defined in ${portTypeNamespace} needed for ${bindingNamespaceAlias}->${bindingName}`);
}
const portTypeAliasMap = this.aliasMap.get(portTypeObj);
for (const operationObj of portTypeObj.operation) {
const operationName = operationObj.attributes.name;
const rootTC = isQueryOperationName(operationName)
? this.schemaComposer.Query
: this.schemaComposer.Mutation;
const operationFieldName = sanitizeNameForGraphQL(`${typePrefix}_${serviceName}_${portName}_${operationName}`);
const outputObj = operationObj.output[0];
const [messageNamespaceAlias, messageName] = outputObj.attributes.message.split(':');
const messageNamespace = portTypeAliasMap.get(messageNamespaceAlias);
if (!messageNamespace) {
throw new Error(`Namespace alias: ${messageNamespaceAlias} is undefined!`);
}
const { type, elementName } = this.getOutputTypeForMessage(this.getNamespaceMessageMap(messageNamespace).get(messageName));
const bindingOperationObject = bindingObj.operation.find(operation => operation.attributes.name === operationName);
const soapAnnotations = {
elementName,
bindingNamespace,
endpoint: this.endpoint,
subgraph: this.subgraphName,
soapNamespace: this.soapNamespace,
};
if (!soapAnnotations.endpoint && portObj.address) {
for (const address of portObj.address) {
if (address.attributes) {
for (const attributeName in address.attributes) {
const value = address.attributes[attributeName];
if (value && attributeName.toLowerCase().endsWith('location')) {
soapAnnotations.endpoint = value;
break;
}
}
}
}
}
if (bindingOperationObject?.operation) {
for (const bindingOperationObjectElem of bindingOperationObject.operation) {
if (bindingOperationObjectElem.attributes) {
for (const attributeName in bindingOperationObjectElem.attributes) {
const value = bindingOperationObjectElem.attributes[attributeName];
if (value && attributeName.toLowerCase().endsWith('action')) {
soapAnnotations.soapAction = value;
break;
}
}
}
}
}
if (this.bodyAlias) {
soapAnnotations.bodyAlias = this.bodyAlias;
}
if (this.soapHeaders) {
soapAnnotations.soapHeaders = this.soapHeaders;
}
rootTC.addFields({
[operationFieldName]: {
type,
directives: [
{
name: 'soap',
args: soapAnnotations,
},
],
},
});
const inputObj = operationObj.input[0];
const [inputMessageNamespaceAlias, inputMessageName] = inputObj.attributes.message.split(':');
const inputMessageNamespace = portTypeAliasMap.get(inputMessageNamespaceAlias);
if (!inputMessageNamespace) {
throw new Error(`Namespace alias: ${inputMessageNamespaceAlias} is undefined!`);
}
const inputMessageObj = this.getNamespaceMessageMap(inputMessageNamespace).get(inputMessageName);
if (!inputMessageObj) {
throw new Error(`Message: ${inputMessageName} is not defined in ${inputMessageNamespace} needed for ${portTypeName}->${operationName}`);
}
const aliasMap = this.aliasMap.get(inputMessageObj);
// Collect which message part names the binding routes to soap:Header
const headerPartNames = new Set();
for (const inputBinding of bindingOperationObject?.input ?? []) {
for (const headerElem of inputBinding.header ?? []) {
if (headerElem.attributes?.part) {
headerPartNames.add(headerElem.attributes.part);
}
}
}
// Build per-arg namespace map and header arg list alongside the existing arg registration
const argNamespaceMap = {};
const headerArgNames = [];
for (const part of inputMessageObj.part) {
if (part.attributes.element) {
const [elementNamespaceAlias, elementName] = part.attributes.element.split(':');
const argName = sanitizeNameForGraphQL(elementName);
const elementNs = aliasMap.get(elementNamespaceAlias) ||
part.attributes[elementNamespaceAlias];
if (elementNs) {
argNamespaceMap[argName] = elementNs;
}
if (headerPartNames.has(part.attributes.name || elementName)) {
headerArgNames.push(argName);
}
rootTC.addFieldArgs(operationFieldName, {
[argName]: {
type: () => {
const elementNamespace = aliasMap.get(elementNamespaceAlias) ||
part.attributes[elementNamespaceAlias];
if (!elementNamespace) {
throw new Error(`Namespace alias: ${elementNamespaceAlias} is not defined.`);
}
return this.getInputTypeForTypeNameInNamespace({
typeName: elementName,
typeNamespace: elementNamespace,
});
},
defaultValue: '',
},
});
}
else if (part.attributes.name) {
const partName = part.attributes.name;
const argName = sanitizeNameForGraphQL(partName);
// For RPC-style <wsdl:part name="X" type="ns:T"/>, the element
// wrapper QName is the part name in the binding's namespace —
// the type's namespace describes the value type, not the
// accessor element. Always use bindingNamespace here, never
// part@type's namespace.
if (bindingNamespace) {
argNamespaceMap[argName] = bindingNamespace;
}
if (headerPartNames.has(partName)) {
headerArgNames.push(argName);
}
rootTC.addFieldArgs(operationFieldName, {
[argName]: {
type: () => {
const typeRef = part.attributes.type;
const [typeNamespaceAlias, typeName] = typeRef.split(':');
const typeNamespace = aliasMap.get(typeNamespaceAlias);
if (!typeNamespace) {
throw new Error(`Namespace alias: ${typeNamespaceAlias} is undefined!`);
}
const inputTC = this.getInputTypeForTypeNameInNamespace({
typeName,
typeNamespace,
});
if ('getFields' in inputTC && Object.keys(inputTC.getFields()).length === 0) {
return GraphQLJSON;
}
return inputTC;
},
defaultValue: '',
},
});
}
}
if (headerArgNames.length > 0) {
soapAnnotations.headerArgNames = headerArgNames;
}
if (Object.keys(argNamespaceMap).length > 0) {
soapAnnotations.argNamespaces = argNamespaceMap;
}
}
}
}
}
}
async fetchXSD(location, parentAliasMap = new Map(), baseUrl) {
const resolved = resolveLocation(baseUrl ?? this.cwd, location);
if (this.loadedLocations.has(resolved))
return;
let xsdText = await readFileOrUrl(resolved, {
allowUnknownExtensions: true,
cwd: this.cwd,
fetch: this.fetchFn,
importFn: defaultImportFn,
logger: this.logger,
headers: this.schemaHeadersFactory({ env: process.env }),
});
xsdText = xsdText.split('xmlns:').join('namespace:');
// WSDL Import is different than XS Import
const xsdObj = this.xmlParser.parse(xsdText, PARSE_XML_OPTIONS);
// Pre-register before recursing so circular imports terminate.
this.loadedLocations.set(resolved, xsdObj);
for (const schemaObj of xsdObj.schema) {
await this.loadSchema(schemaObj, parentAliasMap, resolved);
}
}
async loadWSDL(wsdlText, baseUrl) {
wsdlText = wsdlText.split('xmlns:').join('namespace:');
let wsdlObject;
try {
wsdlObject = this.xmlParser.parse(wsdlText, PARSE_XML_OPTIONS);
}
catch (e) {
throw new Error(`Failed to parse WSDL: ${e.message}. \nReturned response;\n${wsdlText}`);
}
if (!Array.isArray(wsdlObject.definitions)) {
throw new Error(`WSDL definitions not found! Please make sure if your WSDL source is correct, and it contains <definitions> tag.\nReturned response;\n${wsdlText}`);
}
// Anchor a relative filesystem baseUrl against this.cwd. Without this, a
// caller passing a relative source (e.g. "./wsdl/foo.wsdl") would have
// path.resolve fall back to process.cwd() — which can differ from the
// loader's working directory in monorepos / containers, sending nested
// imports to the wrong absolute paths.
let resolvedBase = baseUrl;
if (resolvedBase &&
!URI_SCHEME_RE.test(resolvedBase) &&
!path.isAbsolute(resolvedBase) &&
this.cwd) {
resolvedBase = path.resolve(this.cwd, resolvedBase);
}
// Pre-register before recursing so circular imports terminate.
if (resolvedBase) {
this.loadedLocations.set(resolvedBase, wsdlObject);
}
for (const definition of wsdlObject.definitions) {
await this.loadDefinition(definition, resolvedBase);
}
return wsdlObject;
}
async fetchWSDL(location, baseUrl) {
const resolved = resolveLocation(baseUrl ?? this.cwd, location);
if (this.loadedLocations.has(resolved))
return;
const wsdlText = await readFileOrUrl(resolved, {
allowUnknownExtensions: true,
cwd: this.cwd,
fetch: this.fetchFn,
importFn: defaultImportFn,
logger: this.logger,
headers: this.schemaHeadersFactory({ env: process.env }),
});
await this.loadWSDL(wsdlText, resolved);
}
getAliasMapFromAttributes(attributes) {
const aliasMap = new Map();
for (const attributeName in attributes) {
const attributeValue = attributes[attributeName];
if (attributeName !== 'targetNamespace') {
aliasMap.set(attributeName, attributeValue);
}
}
return aliasMap;
}
getTypeForSimpleType(simpleType, simpleTypeNamespace) {
let simpleTypeTC = this.simpleTypeTCMap.get(simpleType);
if (!simpleTypeTC) {
const simpleTypeName = sanitizeNameForGraphQL(simpleType.attributes.name);
const restrictionObj = simpleType.restriction[0];
const prefix = this.namespaceTypePrefixMap.get(simpleTypeNamespace);
if (restrictionObj.enumeration) {
const enumTypeName = `${prefix}_${simpleTypeName}`;
const values = {};
for (const enumerationObj of restrictionObj.enumeration) {
const enumValue = enumerationObj.attributes.value;
const enumKey = sanitizeNameForGraphQL(enumValue);
values[enumKey] = {
value: enumValue,
};
}
simpleTypeTC = this.schemaComposer.createEnumTC({
name: enumTypeName,
values,
});
}
else if (restrictionObj.pattern) {
const patternObj = restrictionObj.pattern[0];
const pattern = patternObj.attributes.value;
const scalarTypeName = `${prefix}_${simpleTypeName}`;
simpleTypeTC = this.schemaComposer.createScalarTC(new RegularExpression(scalarTypeName, new RegExp(pattern)));
}
else {
// TODO: Other restrictions are not supported yet
const aliasMap = this.aliasMap.get(simpleType);
const [baseTypeNamespaceAlias, baseTypeName] = restrictionObj.attributes.base.split(':');
const baseTypeNamespace = aliasMap.get(baseTypeNamespaceAlias);
if (!baseTypeNamespace) {
throw new Error(`Invalid base type namespace: ${baseTypeNamespaceAlias}`);
}
const baseType = this.getNamespaceSimpleTypeMap(baseTypeNamespace)?.get(baseTypeName);
if (!baseType) {
throw new Error(`Simple Type: ${baseTypeName} couldn't be found in ${baseTypeNamespace} needed for ${simpleTypeName}`);
}
simpleTypeTC = this.getTypeForSimpleType(baseType, baseTypeNamespace);
}
this.simpleTypeTCMap.set(simpleType, simpleTypeTC);
}
return simpleTypeTC;
}
getInputTypeForTypeNameInNamespace({ typeName, typeNamespace, }) {
// Check element aliases first — consistent with the eager path that overwrites the type map
const elementRef = this.namespaceElementRefMap.get(typeNamespace)?.get(typeName);
if (elementRef) {
return this.getInputTypeForTypeNameInNamespace(elementRef);
}
const complexType = this.getNamespaceComplexTypeMap(typeNamespace)?.get(typeName);
if (complexType) {
return this.getInputTypeForComplexType(complexType, typeNamespace);
}
const simpleType = this.getNamespaceSimpleTypeMap(typeNamespace)?.get(typeName);
if (simpleType) {
return this.getTypeForSimpleType(simpleType, typeNamespace);
}
throw new Error(`Type: ${typeName} couldn't be found in ${typeNamespace}`);
}
getInputTypeForComplexType(complexType, complexTypeNamespace) {
let complexTypeTC = this.complexTypeInputTCMap.get(complexType);
if (!complexTypeTC) {
const complexTypeName = sanitizeNameForGraphQL(complexType.attributes.name);
const prefix = this.namespaceTypePrefixMap.get(complexTypeNamespace);
const aliasMap = this.aliasMap.get(complexType);
const fieldMap = {};
const choiceOrSequenceObjects = [
...(complexType.sequence || []),
...(complexType.choice || []),
];
for (const sequenceOrChoiceObj of choiceOrSequenceObjects) {
if (sequenceOrChoiceObj.element) {
for (const elementObj of sequenceOrChoiceObj.element) {
if (elementObj.attributes?.name) {
const fieldName = sanitizeNameForGraphQL(elementObj.attributes.name);
fieldMap[fieldName] = {
type: () => {
const maxOccurs = sequenceOrChoiceObj.attributes?.maxOccurs || elementObj.attributes?.maxOccurs;
const minOccurs = sequenceOrChoiceObj.attributes?.minOccurs || elementObj.attributes?.minOccurs;
const nillable = sequenceOrChoiceObj.attributes?.nillable || elementObj.attributes?.nillable;
const isPlural = maxOccurs != null && maxOccurs !== '1';
let isNullable = false;
if (minOccurs == null || minOccurs === '0') {
isNullable = true;
}
if (nillable === 'true') {
isNullable = true;
}
if (nillable === 'false') {
isNullable = false;
}
if (elementObj.attributes?.type) {
const [typeNamespaceAlias, typeName] = elementObj.attributes.type.split(':');
let typeNamespace;
if (elementObj.attributes[typeNamespaceAlias]) {
typeNamespace = elementObj.attributes[typeNamespaceAlias];
}
else {
typeNamespace = aliasMap.get(typeNamespaceAlias);
}
if (!typeNamespace) {
throw new Error(`Namespace alias: ${typeNamespaceAlias} is undefined!`);
}
let finalTC = this.getInputTypeForTypeNameInNamespace({
typeName,
typeNamespace,
});
if (isPlural) {
finalTC = finalTC.getTypePlural();
}
if (!isNullable) {
finalTC = finalTC.getTypeNonNull();
}
return finalTC;
}
else if (elementObj.simpleType) {
// eslint-disable-next-line no-unreachable-loop
for (const simpleTypeObj of elementObj.simpleType) {
// Dynamically defined simple type
// So we need to define alias map for this type
this.aliasMap.set(simpleTypeObj, aliasMap);
// Inherit the name from elementObj, scoped to the parent type name
// to avoid collisions when sibling types share an inline field name.
// Skip scoping when complexTypeName already equals the namespace prefix
// (i.e. it is a top-level-element anonymous type) to avoid double-prefixing
// like ByNameDataSet_ByNameDataSet_ByName.
simpleTypeObj.attributes = simpleTypeObj.attributes || {};
simpleTypeObj.attributes.name =
simpleTypeObj.attributes.name ||
(complexTypeName !== prefix
? `${complexTypeName}_${elementObj.attributes.name}`
: elementObj.attributes.name);
let finalTC = this.getTypeForSimpleType(simpleTypeObj, complexTypeNamespace);
if (isPlural) {
finalTC = finalTC.getTypePlural();
}
if (!isNullable) {
finalTC = finalTC.getTypeNonNull();
}
return finalTC;
}
}
else if (elementObj.complexType) {
// eslint-disable-next-line no-unreachable-loop
for (const complexTypeObj of elementObj.complexType) {
// Dynamically defined type
// So we need to define alias map for this type
this.aliasMap.set(complexTypeObj, aliasMap);
// Inherit the name from elementObj, scoped to the parent type name
// to avoid collisions when sibling types share an inline field name.
// Skip scoping when complexTypeName already equals the namespace prefix
// (i.e. it is a top-level-element anonymous type) to avoid double-prefixing.
complexTypeObj.attributes = complexTypeObj.attributes || {};
complexTypeObj.attributes.name =
complexTypeObj.attributes.name ||
(complexTypeName !== prefix
? `${complexTypeName}_${elementObj.attributes.name}`
: elementObj.attributes.name);
let finalTC = this.getInputTypeForComplexType(complexTypeObj, complexTypeNamespace);
if (isPlural) {
finalTC = finalTC.getTypePlural();
}
if (!isNullable) {
finalTC = finalTC.getTypeNonNull();
}
return finalTC;
}
}
throw new Error(`Invalid element type definition: ${complexTypeName}->${fieldName}`);
},
};
}
else {
if (elementObj.attributes?.ref) {
this.logger.warn(`element.ref isn't supported yet.`);
}
else {
this.logger.warn(`Element doesn't have a name in ${complexTypeName}. Ignoring...`);
}
}
}
}
if (sequenceOrChoiceObj.any) {
for (const anyObj of sequenceOrChoiceObj.any) {
// namespace may be a space-delimited list; trim each token and skip XSD wildcards
const anyNamespaces = (anyObj.attributes?.namespace ?? '')
.split(/\s+/)
.map((s) => s.trim())
.filter((ns) => ns && !ns.startsWith('##'));
for (const anyNamespace of anyNamespaces) {
const anyTypeTC = this.getInputTypeForTypeNameInNamespace({
typeName: complexType.attributes.name,
typeNamespace: anyNamespace,
});
if ('getFields' in anyTypeTC) {
for (const fieldName in anyTypeTC.getFields()) {
fieldMap[fieldName] = anyTypeTC.getField(fieldName);
}
}
}
}
}
}
if (complexType.complexContent) {
for (const complexContentObj of complexType.complexContent) {
for (const extensionObj of complexContentObj.extension) {
const [baseTypeNamespaceAlias, baseTypeName] = extensionObj.attributes.base.split(':');
let baseTypeNamespace;
if (extensionObj.attributes[baseTypeNamespaceAlias]) {
baseTypeNamespace = extensionObj.attributes[baseTypeNamespaceAlias];
}
else {
baseTypeNamespace = aliasMap.get(baseTypeNamespaceAlias);
}
if (!baseTypeNamespace) {
throw new Error(`Name