diff --git a/.changeset/@omnigraph_soap-8196-dependencies.md b/.changeset/@omnigraph_soap-8196-dependencies.md
new file mode 100644
index 0000000000000..13e075d488777
--- /dev/null
+++ b/.changeset/@omnigraph_soap-8196-dependencies.md
@@ -0,0 +1,5 @@
+---
+"@omnigraph/soap": patch
+---
+dependencies updates:
+ - Added dependency [`@graphql-mesh/transport-common@^0.7.25` ↗︎](https://www.npmjs.com/package/@graphql-mesh/transport-common/v/0.7.25) (to `dependencies`)
diff --git a/.changeset/funny-kangaroos-think.md b/.changeset/funny-kangaroos-think.md
new file mode 100644
index 0000000000000..b4889bca1b08a
--- /dev/null
+++ b/.changeset/funny-kangaroos-think.md
@@ -0,0 +1,75 @@
+---
+'@graphql-mesh/transport-soap': patch
+'@omnigraph/soap': patch
+'@graphql-mesh/types': patch
+---
+
+- You can now choose the name of the alias you want to use for SOAP body;
+
+```ts filename="mesh.config.ts" {4}
+import { defineConfig } from '@graphql-mesh/compose-cli'
+
+export const composeConfig = defineConfig({
+ sources: [
+ {
+ sourceHandler: loadSOAPSubgraph('CountryInfo', {
+ source:
+ 'http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL',
+ bodyAlias: 'my-body'
+ })
+ }
+ ]
+})
+```
+
+- Then it will generate a body like below by using the alias;
+
+```xml
+
+
+
+ baz
+
+
+
+```
+
+If you want to add SOAP headers to the request body like below;
+
+```xml
+
+
+
+ user
+ password
+
+
+```
+
+You can add the headers to the configuration like below;
+
+```ts filename="mesh.config.ts" {2,7-9}
+import { defineConfig } from '@graphql-mesh/compose-cli'
+import { loadSOAPSubgraph } from '@omnigraph/soap'
+
+export const composeConfig = defineConfig({
+ subgraphs: [
+ {
+ sourceHandler: loadSOAPSubgraph('CountryInfo', {
+ source:
+ 'http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL',
+ soapHeaders: {
+ alias: 'header',
+ namespace: 'http://foo.com',
+ headers: {
+ MyHeader: {
+ UserName: 'user',
+ Password: 'password'
+ }
+ }
+ }
+ })
+ }
+ ]
+})
+```
diff --git a/e2e/soap-demo/__snapshots__/soap-demo.test.ts.snap b/e2e/soap-demo/__snapshots__/soap-demo.test.ts.snap
index feb4e05a13ed7..823528321ac5e 100644
--- a/e2e/soap-demo/__snapshots__/soap-demo.test.ts.snap
+++ b/e2e/soap-demo/__snapshots__/soap-demo.test.ts.snap
@@ -89,6 +89,8 @@ directive @soap(
bindingNamespace: String
endpoint: String
subgraph: String
+ bodyAlias: String
+ soapHeaders: SOAPHeaders
) repeatable on FIELD_DEFINITION
directive @extraSchemaDefinitionDirective(directives: _DirectiveExtensions) repeatable on OBJECT
@@ -108,6 +110,8 @@ The \`JSON\` scalar type represents JSON values as specified by [ECMA-404](http:
"""
scalar JSON @join__type(graph: SOAP_DEMO)
+scalar ObjMap @join__type(graph: SOAP_DEMO)
+
scalar _DirectiveExtensions @join__type(graph: SOAP_DEMO)
type Query @extraSchemaDefinitionDirective(directives: {transport: [{kind: "soap", subgraph: "soap-demo"}]}) @join__type(graph: SOAP_DEMO) {
@@ -294,6 +298,12 @@ input s0_DivideInteger_Input @join__type(graph: SOAP_DEMO) {
input s0_LookupCity_Input @join__type(graph: SOAP_DEMO) {
zip: String
}
+
+input SOAPHeaders @join__type(graph: SOAP_DEMO) {
+ namespace: String
+ alias: String
+ headers: ObjMap
+}
"
`;
diff --git a/e2e/utils/leftoverStack.ts b/e2e/utils/leftoverStack.ts
index e94c2e4fa3f25..72dc6246155ba 100644
--- a/e2e/utils/leftoverStack.ts
+++ b/e2e/utils/leftoverStack.ts
@@ -17,16 +17,13 @@ function handleSuppressedError(e: any) {
}
if (typeof afterAll === 'function') {
- afterAll(() => {
+ afterAll(async () => {
try {
- const disposeRes$ = leftoverStack.disposeAsync();
- leftoverStack = new AsyncDisposableStack();
- if (disposeRes$?.catch) {
- disposeRes$.catch(handleSuppressedError);
- }
+ await leftoverStack.disposeAsync();
} catch (e) {
handleSuppressedError(e);
+ } finally {
+ leftoverStack = new AsyncDisposableStack();
}
- leftoverStack = new AsyncDisposableStack();
});
}
diff --git a/packages/legacy/handlers/soap/src/index.ts b/packages/legacy/handlers/soap/src/index.ts
index c6df64b6326dd..95080ed786421 100644
--- a/packages/legacy/handlers/soap/src/index.ts
+++ b/packages/legacy/handlers/soap/src/index.ts
@@ -64,6 +64,8 @@ export default class SoapHandler implements MeshHandler {
logger: this.logger,
schemaHeaders: this.config.schemaHeaders,
operationHeaders: this.config.operationHeaders,
+ soapHeaders: this.config.soapHeaders,
+ bodyAlias: this.config.bodyAlias,
});
const wsdlLocation = this.config.source;
const wsdl = await readFileOrUrl(wsdlLocation, {
diff --git a/packages/legacy/handlers/soap/yaml-config.graphql b/packages/legacy/handlers/soap/yaml-config.graphql
index a791ad9a3b42a..0b8d8ae38d1ef 100644
--- a/packages/legacy/handlers/soap/yaml-config.graphql
+++ b/packages/legacy/handlers/soap/yaml-config.graphql
@@ -19,4 +19,33 @@ type SoapHandler @md {
JSON object representing the Headers to add to the runtime of the API calls only for operation during runtime
"""
operationHeaders: JSON
+ """
+ The name of the alias to be used in the envelope for body components
+
+ default: `body`
+ """
+ bodyAlias: String
+ """
+ SOAP Headers to be added to the request
+ """
+ soapHeaders: SOAPHeaders
+}
+
+type SOAPHeaders {
+ """
+ The name of the alias to be used in the envelope
+
+ default: `header`
+ """
+ alias: String
+ """
+ The namespace of the SOAP Header
+ For example: `http://www.example.com/namespace`
+ """
+ namespace: String!
+ """
+ The content of the SOAP Header
+ For example: { "key": "value" } then the content will be `value`
+ """
+ headers: JSON!
}
diff --git a/packages/legacy/types/src/config-schema.json b/packages/legacy/types/src/config-schema.json
index 267134f53cc60..ad6d77bdc9a89 100644
--- a/packages/legacy/types/src/config-schema.json
+++ b/packages/legacy/types/src/config-schema.json
@@ -3004,10 +3004,39 @@
"type": "object",
"properties": {},
"description": "JSON object representing the Headers to add to the runtime of the API calls only for operation during runtime"
+ },
+ "bodyAlias": {
+ "type": "string",
+ "description": "The name of the alias to be used in the envelope for body components\n\ndefault: `body`"
+ },
+ "soapHeaders": {
+ "$ref": "#/definitions/SOAPHeaders",
+ "description": "SOAP Headers to be added to the request"
}
},
"required": ["source"]
},
+ "SOAPHeaders": {
+ "additionalProperties": false,
+ "type": "object",
+ "title": "SOAPHeaders",
+ "properties": {
+ "alias": {
+ "type": "string",
+ "description": "The name of the alias to be used in the envelope\n\ndefault: `header`"
+ },
+ "namespace": {
+ "type": "string",
+ "description": "The namespace of the SOAP Header\nFor example: `http://www.example.com/namespace`"
+ },
+ "headers": {
+ "type": "object",
+ "properties": {},
+ "description": "The content of the SOAP Header\nFor example: { \"key\": \"value\" } then the content will be `value`"
+ }
+ },
+ "required": ["namespace", "headers"]
+ },
"SupergraphHandler": {
"additionalProperties": false,
"type": "object",
diff --git a/packages/legacy/types/src/config.ts b/packages/legacy/types/src/config.ts
index e1287fb3f7528..19ecf8ea41bab 100644
--- a/packages/legacy/types/src/config.ts
+++ b/packages/legacy/types/src/config.ts
@@ -1015,6 +1015,36 @@ export interface SoapHandler {
operationHeaders?: {
[k: string]: any;
};
+ /**
+ * The name of the alias to be used in the envelope for body components
+ *
+ * default: `body`
+ */
+ bodyAlias?: string;
+ soapHeaders?: SOAPHeaders;
+}
+/**
+ * SOAP Headers to be added to the request
+ */
+export interface SOAPHeaders {
+ /**
+ * The name of the alias to be used in the envelope
+ *
+ * default: `header`
+ */
+ alias?: string;
+ /**
+ * The namespace of the SOAP Header
+ * For example: `http://www.example.com/namespace`
+ */
+ namespace: string;
+ /**
+ * The content of the SOAP Header
+ * For example: { "key": "value" } then the content will be `value`
+ */
+ headers: {
+ [k: string]: any;
+ };
}
export interface SupergraphHandler {
/**
diff --git a/packages/loaders/soap/package.json b/packages/loaders/soap/package.json
index e42f4cb05c819..c008b11bb6115 100644
--- a/packages/loaders/soap/package.json
+++ b/packages/loaders/soap/package.json
@@ -37,6 +37,7 @@
"dependencies": {
"@graphql-mesh/cross-helpers": "^0.4.9",
"@graphql-mesh/string-interpolation": "^0.5.7",
+ "@graphql-mesh/transport-common": "^0.7.25",
"@graphql-mesh/transport-soap": "^0.8.10",
"@graphql-mesh/types": "^0.103.10",
"@graphql-mesh/utils": "^0.103.10",
diff --git a/packages/loaders/soap/src/SOAPLoader.ts b/packages/loaders/soap/src/SOAPLoader.ts
index 0e727688b0d9c..9e46ec23f4102 100644
--- a/packages/loaders/soap/src/SOAPLoader.ts
+++ b/packages/loaders/soap/src/SOAPLoader.ts
@@ -5,6 +5,7 @@ import {
GraphQLBoolean,
GraphQLDirective,
GraphQLFloat,
+ GraphQLInputObjectType,
GraphQLInt,
GraphQLString,
} from 'graphql';
@@ -39,6 +40,7 @@ import {
import { process } from '@graphql-mesh/cross-helpers';
import type { ResolverDataBasedFactory } from '@graphql-mesh/string-interpolation';
import { getInterpolatedHeadersFactory } from '@graphql-mesh/string-interpolation';
+import { ObjMapScalar } from '@graphql-mesh/transport-common';
import type { Logger, MeshFetch } from '@graphql-mesh/types';
import {
defaultImportFn,
@@ -71,10 +73,50 @@ export interface SOAPLoaderOptions {
logger?: Logger;
schemaHeaders?: Record;
operationHeaders?: Record;
+ soapHeaders?: SOAPHeaders;
endpoint?: string;
cwd?: string;
+ bodyAlias?: string;
}
+export interface SOAPHeaders {
+ /**
+ * The namespace of the SOAP Header
+ *
+ * @example http://www.example.com/namespace
+ */
+ namespace: string;
+ /**
+ * The name of the alias to be used in the envelope
+ *
+ * @default header
+ */
+ alias?: string;
+ /**
+ * The content of the SOAP Header
+ *
+ * @example { "key": "value" }
+ *
+ * then the content will be `value` in XML
+ */
+ headers: unknown;
+}
+
+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],
@@ -91,6 +133,12 @@ const soapDirective = new GraphQLDirective({
subgraph: {
type: GraphQLString,
},
+ bodyAlias: {
+ type: GraphQLString,
+ },
+ soapHeaders: {
+ type: SOAPHeadersInput,
+ },
},
});
@@ -143,6 +191,8 @@ export class SOAPLoader {
private logger: Logger;
private endpoint?: string;
private cwd: string;
+ private soapHeaders: SOAPHeaders;
+ private bodyAlias?: string;
constructor(options: SOAPLoaderOptions) {
this.fetchFn = options.fetch || defaultFetchFn;
@@ -153,6 +203,8 @@ export class SOAPLoader {
this.schemaHeadersFactory = getInterpolatedHeadersFactory(options.schemaHeaders || {});
this.endpoint = options.endpoint;
this.cwd = options.cwd;
+ this.soapHeaders = options.soapHeaders;
+ this.bodyAlias = options.bodyAlias;
}
loadXMLSchemaNamespace() {
@@ -452,6 +504,12 @@ export class SOAPLoader {
endpoint: this.endpoint || portObj.address[0].attributes.location,
subgraph: this.subgraphName,
};
+ if (this.bodyAlias) {
+ soapAnnotations.bodyAlias = this.bodyAlias;
+ }
+ if (this.soapHeaders) {
+ soapAnnotations.soapHeaders = this.soapHeaders;
+ }
rootTC.addFields({
[operationFieldName]: {
type,
diff --git a/packages/loaders/soap/src/index.ts b/packages/loaders/soap/src/index.ts
index db5912429773a..076561585f6d1 100644
--- a/packages/loaders/soap/src/index.ts
+++ b/packages/loaders/soap/src/index.ts
@@ -1,45 +1,71 @@
import type { Logger, MeshFetch } from '@graphql-mesh/types';
-import { defaultImportFn, DefaultLogger, readFileOrUrl } from '@graphql-mesh/utils';
-import { SOAPLoader } from './SOAPLoader.js';
+import { defaultImportFn, mapMaybePromise, readFileOrUrl } from '@graphql-mesh/utils';
+import { SOAPLoader, type SOAPHeaders } from './SOAPLoader.js';
export * from './SOAPLoader.js';
export type * from './types.js';
export * from '@graphql-mesh/transport-soap';
export interface SOAPSubgraphLoaderOptions {
+ /**
+ * A url to your WSDL or generated SDL with annotations
+ */
source?: string;
+ /**
+ * SOAP endpoint to use for the API calls
+ */
endpoint?: string;
- fetch?: MeshFetch;
- logger?: Logger;
+ /**
+ * JSON object representing the Headers to add to the runtime of the API calls only for schema introspection
+ * You can also provide `.js` or `.ts` file path that exports schemaHeaders as an object
+ */
schemaHeaders?: Record;
+ /**
+ * JSON object representing the Headers to add to the runtime of the API calls only for operation calls
+ */
operationHeaders?: Record;
+ /**
+ * SOAP Headers configuration
+ */
+ soapHeaders?: SOAPHeaders;
+ /**
+ * The name of the alias to be used in the envelope for body components
+
+ * @default body
+ */
+ bodyAlias?: string;
}
export function loadSOAPSubgraph(subgraphName: string, options: SOAPSubgraphLoaderOptions) {
return ({ cwd, fetch, logger }: { cwd: string; fetch: MeshFetch; logger: Logger }) => {
const soapLoader = new SOAPLoader({
subgraphName,
- fetch: options.fetch || fetch,
- logger: options.logger || logger,
+ fetch,
+ logger,
+ cwd,
+ // Configuration from the user
schemaHeaders: options.schemaHeaders,
operationHeaders: options.operationHeaders,
endpoint: options.endpoint,
- cwd,
+ bodyAlias: options.bodyAlias,
+ soapHeaders: options.soapHeaders,
});
return {
name: subgraphName,
- schema$: readFileOrUrl(options.source, {
- allowUnknownExtensions: true,
- cwd,
- fetch: options.fetch || fetch,
- importFn: defaultImportFn,
- logger: new DefaultLogger(`SOAP Subgraph ${subgraphName}`),
- })
- .then(wsdl => soapLoader.loadWSDL(wsdl))
- .then(object => {
- soapLoader.loadedLocations.set(options.source, object);
- return soapLoader.buildSchema();
+ schema$: mapMaybePromise(
+ readFileOrUrl(options.source, {
+ allowUnknownExtensions: true,
+ cwd,
+ fetch,
+ importFn: defaultImportFn,
+ logger,
}),
+ wsdl =>
+ mapMaybePromise(soapLoader.loadWSDL(wsdl), object => {
+ soapLoader.loadedLocations.set(options.source, object);
+ return soapLoader.buildSchema();
+ }),
+ ),
};
};
}
diff --git a/packages/loaders/soap/src/utils.ts b/packages/loaders/soap/src/utils.ts
index 9dc145c6a26bb..b439e617c8cd3 100644
--- a/packages/loaders/soap/src/utils.ts
+++ b/packages/loaders/soap/src/utils.ts
@@ -5,6 +5,12 @@ export interface SoapAnnotations {
elementName: string;
bindingNamespace: string;
endpoint: string;
+ bodyAlias?: string;
+ soapHeaders?: {
+ alias?: string;
+ namespace: string;
+ headers: unknown;
+ };
}
export const PARSE_XML_OPTIONS: Partial = {
diff --git a/packages/loaders/soap/test/__snapshots__/examples.test.ts.snap b/packages/loaders/soap/test/__snapshots__/examples.test.ts.snap
index 9ccbea04a7544..b63ed60716c56 100644
--- a/packages/loaders/soap/test/__snapshots__/examples.test.ts.snap
+++ b/packages/loaders/soap/test/__snapshots__/examples.test.ts.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Examples should generate schema for axis: axis 1`] = `
-"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String) on FIELD_DEFINITION
+"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String, bodyAlias: String, soapHeaders: SOAPHeaders) on FIELD_DEFINITION
type Query {
placeholder: Void
@@ -116,11 +116,19 @@ input impl_createTestRequestType_Input {
input impl_weekDays_Input {
option: [impl_weekday]
-}"
+}
+
+input SOAPHeaders {
+ namespace: String
+ alias: String
+ headers: ObjMap
+}
+
+scalar ObjMap"
`;
exports[`Examples should generate schema for example1: example1 1`] = `
-"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String) on FIELD_DEFINITION
+"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String, bodyAlias: String, soapHeaders: SOAPHeaders) on FIELD_DEFINITION
type Query {
TodoService_TodoService_BasicHttpBinding_ITodoService_GetTodos(GetTodos: JSON = ""): TodoService_GetTodosResponse
@@ -244,11 +252,19 @@ input TodoService_FaultAddTodo_Input {
type TodoService_FailGetTodoResponse {
FailGetTodoResult: String
-}"
+}
+
+input SOAPHeaders {
+ namespace: String
+ alias: String
+ headers: ObjMap
+}
+
+scalar ObjMap"
`;
exports[`Examples should generate schema for example2: example2 1`] = `
-"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String) on FIELD_DEFINITION
+"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String, bodyAlias: String, soapHeaders: SOAPHeaders) on FIELD_DEFINITION
type Query {
AdminServiceType_AdminServiceType_BasicHttpBinding_IAdminService_GetServiceHealth(GetServiceHealth: JSON = ""): tns_GetServiceHealthResponse
@@ -470,11 +486,19 @@ input AdminServiceType_DeleteFileClass_Input {
input AdminServiceType_DeleteEntityClass_Input {
entityclassIntID: BigInt
-}"
+}
+
+input SOAPHeaders {
+ namespace: String
+ alias: String
+ headers: ObjMap
+}
+
+scalar ObjMap"
`;
exports[`Examples should generate schema for greeting: greeting 1`] = `
-"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String) on FIELD_DEFINITION
+"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String, bodyAlias: String, soapHeaders: SOAPHeaders) on FIELD_DEFINITION
type Query {
placeholder: Void
@@ -507,11 +531,19 @@ type NumberConversion_NumberToDollarsResponse {
input NumberConversion_NumberToDollars_Input {
dNum: Float
-}"
+}
+
+input SOAPHeaders {
+ namespace: String
+ alias: String
+ headers: ObjMap
+}
+
+scalar ObjMap"
`;
exports[`Examples should generate schema for tempconvert: tempconvert 1`] = `
-"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String) on FIELD_DEFINITION
+"directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String, bodyAlias: String, soapHeaders: SOAPHeaders) on FIELD_DEFINITION
type Query {
placeholder: Void
@@ -543,5 +575,13 @@ type tns_CelsiusToFahrenheitResponse {
input tns_CelsiusToFahrenheit_Input {
Celsius: String
-}"
+}
+
+input SOAPHeaders {
+ namespace: String
+ alias: String
+ headers: ObjMap
+}
+
+scalar ObjMap"
`;
diff --git a/packages/loaders/soap/test/__snapshots__/soap.test.ts.snap b/packages/loaders/soap/test/__snapshots__/soap.test.ts.snap
index 9e741fdbaa932..2bb3e055fe5ae 100644
--- a/packages/loaders/soap/test/__snapshots__/soap.test.ts.snap
+++ b/packages/loaders/soap/test/__snapshots__/soap.test.ts.snap
@@ -6,7 +6,7 @@ exports[`SOAP Loader should create executor for a service with mutations and que
mutation: Mutation
}
-directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String) on FIELD_DEFINITION
+directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String, bodyAlias: String, soapHeaders: SOAPHeaders) on FIELD_DEFINITION
type Query {
placeholder: Void
@@ -39,5 +39,13 @@ type NumberConversion_NumberToDollarsResponse {
input NumberConversion_NumberToDollars_Input {
dNum: Float
-}"
+}
+
+input SOAPHeaders {
+ namespace: String
+ alias: String
+ headers: ObjMap
+}
+
+scalar ObjMap"
`;
diff --git a/packages/transports/rest/src/directives/getTypeResolverForAbstractType.ts b/packages/transports/rest/src/directives/getTypeResolverForAbstractType.ts
index c34714ad07446..86d8c5ea8f5db 100644
--- a/packages/transports/rest/src/directives/getTypeResolverForAbstractType.ts
+++ b/packages/transports/rest/src/directives/getTypeResolverForAbstractType.ts
@@ -107,6 +107,7 @@ export function getTypeResolverForAbstractType({
if (data.$response) {
const error = createGraphQLError(`Upstream HTTP Error: ${data.$statusCode}`, {
extensions: {
+ code: 'DOWNSTREAM_SERVICE_ERROR',
request: {
url: data.$url,
method: data.$method,
diff --git a/packages/transports/rest/src/directives/httpOperation.ts b/packages/transports/rest/src/directives/httpOperation.ts
index c22a04dca461d..7285573da0acd 100644
--- a/packages/transports/rest/src/directives/httpOperation.ts
+++ b/packages/transports/rest/src/directives/httpOperation.ts
@@ -346,6 +346,7 @@ export function addHTTPRootFieldResolver(
`Upstream HTTP Error: ${response.status}, Could not invoke operation ${httpMethod} ${path}`,
{
extensions: {
+ code: 'DOWNSTREAM_SERVICE_ERROR',
subgraph: sourceName,
request: {
url: fullPath,
@@ -371,6 +372,7 @@ export function addHTTPRootFieldResolver(
`Upstream HTTP Error: ${response.status}, Could not invoke operation ${httpMethod} ${path}`,
{
extensions: {
+ code: 'DOWNSTREAM_SERVICE_ERROR',
subgraph: sourceName,
request: {
url: fullPath,
diff --git a/packages/transports/soap/src/executor.ts b/packages/transports/soap/src/executor.ts
index c8b3a60637f47..014871c6e0cae 100644
--- a/packages/transports/soap/src/executor.ts
+++ b/packages/transports/soap/src/executor.ts
@@ -7,8 +7,11 @@ import type {
} from 'graphql';
import { isListType, isNonNullType } from 'graphql';
import { process } from '@graphql-mesh/cross-helpers';
-import type { ResolverDataBasedFactory } from '@graphql-mesh/string-interpolation';
-import { getInterpolatedHeadersFactory } from '@graphql-mesh/string-interpolation';
+import type { ResolverData, ResolverDataBasedFactory } from '@graphql-mesh/string-interpolation';
+import {
+ getInterpolatedHeadersFactory,
+ stringInterpolator,
+} from '@graphql-mesh/string-interpolation';
import type { MeshFetch } from '@graphql-mesh/types';
import { normalizedExecutor } from '@graphql-tools/executor';
import {
@@ -86,6 +89,12 @@ interface SoapAnnotations {
endpoint: string;
bindingNamespace: string;
elementName: string;
+ bodyAlias?: string;
+ soapHeaders?: {
+ alias?: string;
+ namespace: string;
+ headers: Record;
+ };
}
interface CreateRootValueMethodOpts {
@@ -96,6 +105,33 @@ interface CreateRootValueMethodOpts {
operationHeadersFactory: ResolverDataBasedFactory>;
}
+function prefixWithAlias({
+ alias,
+ obj,
+ resolverData,
+}: {
+ alias: string;
+ obj: unknown;
+ resolverData?: ResolverData;
+}): Record {
+ if (typeof obj === 'object' && obj !== null) {
+ const prefixedHeaderObj: Record = {};
+ for (const key in obj) {
+ const aliasedKey = key === 'innerText' ? key : `${alias}:${key}`;
+ prefixedHeaderObj[aliasedKey] = prefixWithAlias({
+ alias,
+ obj: obj[key],
+ resolverData,
+ });
+ }
+ return prefixedHeaderObj;
+ }
+ if (typeof obj === 'string' && resolverData) {
+ return stringInterpolator.parse(obj, resolverData);
+ }
+ return obj;
+}
+
function createRootValueMethod({
soapAnnotations,
fetchFn,
@@ -104,18 +140,48 @@ function createRootValueMethod({
operationHeadersFactory,
}: CreateRootValueMethodOpts): RootValueMethod {
return async function rootValueMethod(args: any, context: any, info: GraphQLResolveInfo) {
+ const envelopeAttributes: Record = {
+ 'xmlns:soap': 'http://www.w3.org/2003/05/soap-envelope',
+ };
+ const envelope: Record = {
+ attributes: envelopeAttributes,
+ };
+ const resolverData: ResolverData = {
+ args,
+ context,
+ info,
+ env: process.env,
+ };
+
+ const bodyPrefix = soapAnnotations.bodyAlias || 'body';
+ envelopeAttributes[`xmlns:${bodyPrefix}`] = soapAnnotations.bindingNamespace;
+
+ const headerPrefix =
+ soapAnnotations.soapHeaders?.alias || soapAnnotations.bodyAlias || 'header';
+ if (soapAnnotations.soapHeaders?.headers) {
+ envelope['soap:Header'] = prefixWithAlias({
+ alias: headerPrefix,
+ obj: normalizeArgsForConverter(
+ typeof soapAnnotations.soapHeaders.headers === 'string'
+ ? JSON.parse(soapAnnotations.soapHeaders.headers)
+ : soapAnnotations.soapHeaders.headers,
+ ),
+ resolverData,
+ });
+ if (soapAnnotations.soapHeaders?.namespace) {
+ envelopeAttributes[`xmlns:${headerPrefix}`] = soapAnnotations.soapHeaders.namespace;
+ }
+ }
+
+ const body = prefixWithAlias({
+ alias: bodyPrefix,
+ obj: normalizeArgsForConverter(args),
+ resolverData,
+ });
+ envelope['soap:Body'] = body;
+
const requestJson = {
- 'soap:Envelope': {
- attributes: {
- 'xmlns:soap': 'http://www.w3.org/2003/05/soap-envelope',
- },
- 'soap:Body': {
- attributes: {
- xmlns: soapAnnotations.bindingNamespace,
- },
- ...normalizeArgsForConverter(args),
- },
- },
+ 'soap:Envelope': envelope,
};
const requestXML = jsonToXMLConverter.build(requestJson);
const currentFetchFn = context?.fetch || fetchFn;
@@ -141,6 +207,7 @@ function createRootValueMethod({
if (!response.ok) {
return createGraphQLError(`Upstream HTTP Error: ${response.status}`, {
extensions: {
+ code: 'DOWNSTREAM_SERVICE_ERROR',
subgraph: soapAnnotations.subgraph,
request: {
url: soapAnnotations.endpoint,
diff --git a/packages/transports/soap/tests/__snapshots__/headers.spec.ts.snap b/packages/transports/soap/tests/__snapshots__/headers.spec.ts.snap
new file mode 100644
index 0000000000000..21412ae61fd2b
--- /dev/null
+++ b/packages/transports/soap/tests/__snapshots__/headers.spec.ts.snap
@@ -0,0 +1,45 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SOAP Headers should pass headers to the executor: soap-with-headers 1`] = `
+"schema @transport(kind: "soap", subgraph: "Test") {
+ query: Query
+}
+
+directive @soap(elementName: String, bindingNamespace: String, endpoint: String, subgraph: String, bodyAlias: String, soapHeaders: SOAPHeaders) on FIELD_DEFINITION
+
+type Query {
+ tns_GlobalWeather_GlobalWeatherSoap_GetWeather(GetWeather: tns_GetWeather_Input): tns_GetWeatherResponse @soap(elementName: "GetWeatherResponse", bindingNamespace: "http://www.webserviceX.NET", endpoint: "http://www.webservicex.com/globalweather.asmx", subgraph: "Test", bodyAlias: "guild", soapHeaders: {namespace: "https://the-guild.dev", alias: "guild", headers: "{\\"MyHeader\\":{\\"UserName\\":\\"{context.USER_NAME}\\",\\"Password\\":\\"{context.PASSWORD}\\"}}"})
+ tns_GlobalWeather_GlobalWeatherSoap_GetCitiesByCountry(GetCitiesByCountry: tns_GetCitiesByCountry_Input): tns_GetCitiesByCountryResponse @soap(elementName: "GetCitiesByCountryResponse", bindingNamespace: "http://www.webserviceX.NET", endpoint: "http://www.webservicex.com/globalweather.asmx", subgraph: "Test", bodyAlias: "guild", soapHeaders: {namespace: "https://the-guild.dev", alias: "guild", headers: "{\\"MyHeader\\":{\\"UserName\\":\\"{context.USER_NAME}\\",\\"Password\\":\\"{context.PASSWORD}\\"}}"})
+ tns_GlobalWeather_GlobalWeatherSoap12_GetWeather(GetWeather: tns_GetWeather_Input): tns_GetWeatherResponse @soap(elementName: "GetWeatherResponse", bindingNamespace: "http://www.webserviceX.NET", endpoint: "http://www.webservicex.com/globalweather.asmx", subgraph: "Test", bodyAlias: "guild", soapHeaders: {namespace: "https://the-guild.dev", alias: "guild", headers: "{\\"MyHeader\\":{\\"UserName\\":\\"{context.USER_NAME}\\",\\"Password\\":\\"{context.PASSWORD}\\"}}"})
+ tns_GlobalWeather_GlobalWeatherSoap12_GetCitiesByCountry(GetCitiesByCountry: tns_GetCitiesByCountry_Input): tns_GetCitiesByCountryResponse @soap(elementName: "GetCitiesByCountryResponse", bindingNamespace: "http://www.webserviceX.NET", endpoint: "http://www.webservicex.com/globalweather.asmx", subgraph: "Test", bodyAlias: "guild", soapHeaders: {namespace: "https://the-guild.dev", alias: "guild", headers: "{\\"MyHeader\\":{\\"UserName\\":\\"{context.USER_NAME}\\",\\"Password\\":\\"{context.PASSWORD}\\"}}"})
+ tns_GlobalWeather_GlobalWeatherHttpGet_GetWeather(CityName: String = "", CountryName: String = ""): String @soap(elementName: "string", bindingNamespace: "http://www.webserviceX.NET", endpoint: "http://www.webservicex.com/globalweather.asmx", subgraph: "Test", bodyAlias: "guild", soapHeaders: {namespace: "https://the-guild.dev", alias: "guild", headers: "{\\"MyHeader\\":{\\"UserName\\":\\"{context.USER_NAME}\\",\\"Password\\":\\"{context.PASSWORD}\\"}}"})
+ tns_GlobalWeather_GlobalWeatherHttpGet_GetCitiesByCountry(CountryName: String = ""): String @soap(elementName: "string", bindingNamespace: "http://www.webserviceX.NET", endpoint: "http://www.webservicex.com/globalweather.asmx", subgraph: "Test", bodyAlias: "guild", soapHeaders: {namespace: "https://the-guild.dev", alias: "guild", headers: "{\\"MyHeader\\":{\\"UserName\\":\\"{context.USER_NAME}\\",\\"Password\\":\\"{context.PASSWORD}\\"}}"})
+ tns_GlobalWeather_GlobalWeatherHttpPost_GetWeather(CityName: String = "", CountryName: String = ""): String @soap(elementName: "string", bindingNamespace: "http://www.webserviceX.NET", endpoint: "http://www.webservicex.com/globalweather.asmx", subgraph: "Test", bodyAlias: "guild", soapHeaders: {namespace: "https://the-guild.dev", alias: "guild", headers: "{\\"MyHeader\\":{\\"UserName\\":\\"{context.USER_NAME}\\",\\"Password\\":\\"{context.PASSWORD}\\"}}"})
+ tns_GlobalWeather_GlobalWeatherHttpPost_GetCitiesByCountry(CountryName: String = ""): String @soap(elementName: "string", bindingNamespace: "http://www.webserviceX.NET", endpoint: "http://www.webservicex.com/globalweather.asmx", subgraph: "Test", bodyAlias: "guild", soapHeaders: {namespace: "https://the-guild.dev", alias: "guild", headers: "{\\"MyHeader\\":{\\"UserName\\":\\"{context.USER_NAME}\\",\\"Password\\":\\"{context.PASSWORD}\\"}}"})
+}
+
+type tns_GetWeatherResponse {
+ GetWeatherResult: String
+}
+
+input tns_GetWeather_Input {
+ CityName: String
+ CountryName: String
+}
+
+type tns_GetCitiesByCountryResponse {
+ GetCitiesByCountryResult: String
+}
+
+input tns_GetCitiesByCountry_Input {
+ CountryName: String
+}
+
+input SOAPHeaders {
+ namespace: String
+ alias: String
+ headers: ObjMap
+}
+
+scalar ObjMap"
+`;
diff --git a/packages/transports/soap/tests/fixtures/globalweather.wsdl b/packages/transports/soap/tests/fixtures/globalweather.wsdl
new file mode 100644
index 0000000000000..7065646f0c606
--- /dev/null
+++ b/packages/transports/soap/tests/fixtures/globalweather.wsdl
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Get weather report for all major cities around the world.
+
+
+
+
+
+ Get all major
+ cities by country name(full / part).
+
+
+
+
+
+
+
+ Get weather report for all major cities around the world.
+
+
+
+
+
+ Get all major
+ cities by country name(full / part).
+
+
+
+
+
+
+
+ Get weather report for all major cities around the world.
+
+
+
+
+
+ Get all major
+ cities by country name(full / part).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/transports/soap/tests/headers.spec.ts b/packages/transports/soap/tests/headers.spec.ts
new file mode 100644
index 0000000000000..a2e8dd400222c
--- /dev/null
+++ b/packages/transports/soap/tests/headers.spec.ts
@@ -0,0 +1,78 @@
+import { readFileSync } from 'fs';
+import { join } from 'path';
+import { parse } from 'graphql';
+import type { MeshFetch } from '@graphql-mesh/types';
+import { printSchemaWithDirectives } from '@graphql-tools/utils';
+import { createExecutorFromSchemaAST, SOAPLoader } from '@omnigraph/soap';
+import { fetch, Response } from '@whatwg-node/fetch';
+import { dummyLogger as logger } from '../../../testing/dummyLogger';
+
+describe('SOAP Headers', () => {
+ it('should pass headers to the executor', async () => {
+ const soapLoader = new SOAPLoader({
+ subgraphName: 'Test',
+ fetch,
+ logger,
+ bodyAlias: 'guild',
+ soapHeaders: {
+ alias: 'guild',
+ namespace: 'https://the-guild.dev',
+ headers: {
+ MyHeader: {
+ UserName: '{context.USER_NAME}',
+ Password: '{context.PASSWORD}',
+ },
+ },
+ },
+ });
+ await soapLoader.loadWSDL(
+ readFileSync(join(__dirname, './fixtures/globalweather.wsdl'), 'utf-8'),
+ );
+ const schema = soapLoader.buildSchema();
+ expect(printSchemaWithDirectives(schema)).toMatchSnapshot('soap-with-headers');
+ const fetchSpy = jest.fn((_url: string, _init: RequestInit) => Response.error());
+ const executor = createExecutorFromSchemaAST(schema, fetchSpy);
+ await executor({
+ document: parse(/* GraphQL */ `
+ {
+ tns_GlobalWeather_GlobalWeatherSoap_GetWeather(
+ GetWeather: { CityName: "Rome", CountryName: "Italy" }
+ ) {
+ GetWeatherResult
+ }
+ }
+ `),
+ context: {
+ USER_NAME: 'user',
+ PASSWORD: 'password',
+ },
+ });
+ expect(fetchSpy.mock.calls[0][1].body).toBe(
+ `
+${`
+
+
+
+ user
+
+
+ password
+
+
+
+
+
+
+ Rome
+
+
+ Italy
+
+
+ `
+ .trim()
+ .replace(/\n\s+/g, '')}
+ `.trim(),
+ );
+ });
+});
diff --git a/website/src/generated-markdown/SoapHandler.generated.md b/website/src/generated-markdown/SoapHandler.generated.md
index f08c2c7c03c64..3faa58b01ec89 100644
--- a/website/src/generated-markdown/SoapHandler.generated.md
+++ b/website/src/generated-markdown/SoapHandler.generated.md
@@ -2,4 +2,15 @@
* `source` (type: `String`, required) - A url to your WSDL or generated SDL with annotations
* `schemaHeaders` (type: `Any`) - JSON object representing the Headers to add to the runtime of the API calls only for schema introspection
You can also provide `.js` or `.ts` file path that exports schemaHeaders as an object
-* `operationHeaders` (type: `JSON`) - JSON object representing the Headers to add to the runtime of the API calls only for operation during runtime
\ No newline at end of file
+* `operationHeaders` (type: `JSON`) - JSON object representing the Headers to add to the runtime of the API calls only for operation during runtime
+* `bodyAlias` (type: `String`) - The name of the alias to be used in the envelope for body components
+
+default: `body`
+* `soapHeaders` (type: `Object`) - SOAP Headers to be added to the request:
+ * `alias` (type: `String`) - The name of the alias to be used in the envelope
+
+default: `header`
+ * `namespace` (type: `String`, required) - The namespace of the SOAP Header
+For example: `http://www.example.com/namespace`
+ * `headers` (type: `JSON`, required) - The content of the SOAP Header
+For example: { "key": "value" } then the content will be `value`
\ No newline at end of file
diff --git a/website/src/pages/docs/handlers/soap.mdx b/website/src/pages/docs/handlers/soap.mdx
index 6b0c71709e0bb..f3c1664ed1fa4 100644
--- a/website/src/pages/docs/handlers/soap.mdx
+++ b/website/src/pages/docs/handlers/soap.mdx
@@ -27,6 +27,61 @@ sources:
source: http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL
```
+## Headers
+
+If you want to add SOAP headers to the request body like below;
+
+```xml
+
+
+
+ user
+ password
+
+
+```
+
+You can add the headers to the configuration like below;
+
+```yaml
+sources:
+ - name: CountryInfo
+ handler:
+ soap:
+ source: ...
+ soapHeaders:
+ namespace: http://foo.com
+ headers:
+ MyHeader:
+ UserName: user
+ Password: password
+```
+
+## Body Alias
+
+You can now choose the name of the alias you want to use for SOAP body;
+
+```yaml filename=".meshrc.yaml" {4}
+sources:
+ - name: CountryInfo
+ handler:
+ soap:
+ source: ...
+ bodyAlias: my-body
+```
+
+Then it will generate a body like below by using the alias;
+
+```xml
+
+
+
+ baz
+
+
+
+```
+
## CodeSandBox Example
You can check out our example that uses SOAP Handler.
diff --git a/website/src/pages/v1/source-handlers/soap.mdx b/website/src/pages/v1/source-handlers/soap.mdx
index 9601e4c097cbf..1fc3350ea8a28 100644
--- a/website/src/pages/v1/source-handlers/soap.mdx
+++ b/website/src/pages/v1/source-handlers/soap.mdx
@@ -37,7 +37,85 @@ export const composeConfig = defineConfig({
})
```
+## Headers
+
+If you want to add SOAP headers to the request body like below;
+
+```xml
+
+
+
+ user
+ password
+
+
+```
+
+You can add the headers to the configuration like below;
+
+```ts filename="mesh.config.ts" {2,7-9}
+import { defineConfig } from '@graphql-mesh/compose-cli'
+import { loadSOAPSubgraph } from '@omnigraph/soap'
+
+export const composeConfig = defineConfig({
+ subgraphs: [
+ {
+ sourceHandler: loadSOAPSubgraph('CountryInfo', {
+ source:
+ 'http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL',
+ soapHeaders: {
+ // The name of the alias to be used in the envelope for header components
+ alias: 'header',
+ namespace: 'http://foo.com',
+ headers: {
+ MyHeader: {
+ UserName: 'user',
+ Password: 'password'
+ // You can also use environment variables, so it will get the value on runtime
+ Password: '{env.SOAP_PASSWORD}'
+ }
+ }
+ }
+ })
+ }
+ ]
+})
+```
+
[You can find a working example here](
https://github.com/ardatan/graphql-mesh/tree/main/examples/v1-next/soap-demo )
+
+## Custom Body Alias
+
+You can now choose the name of the alias you want to use for SOAP body;
+
+```ts filename="mesh.config.ts" {4}
+import { defineConfig } from '@graphql-mesh/compose-cli'
+import { loadSOAPSubgraph } from '@omnigraph/soap'
+
+export const composeConfig = defineConfig({
+ subgraphs: [
+ {
+ sourceHandler: loadSOAPSubgraph('CountryInfo', {
+ source:
+ 'http://webservices.oorsprong.org/websamples.countryinfo/CountryInfoService.wso?WSDL',
+ bodyAlias: 'my-body'
+ })
+ }
+ ]
+})
+```
+
+Then it will generate a body like below by using the alias;
+
+```xml
+
+
+
+ baz
+
+
+
+```
diff --git a/yarn.lock b/yarn.lock
index 2295d2d24a67e..395f3efd71985 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7218,7 +7218,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@graphql-mesh/transport-common@npm:^0.7.13, @graphql-mesh/transport-common@npm:^0.7.26":
+"@graphql-mesh/transport-common@npm:^0.7.13, @graphql-mesh/transport-common@npm:^0.7.25, @graphql-mesh/transport-common@npm:^0.7.26":
version: 0.7.26
resolution: "@graphql-mesh/transport-common@npm:0.7.26"
dependencies:
@@ -10387,6 +10387,7 @@ __metadata:
dependencies:
"@graphql-mesh/cross-helpers": "npm:^0.4.9"
"@graphql-mesh/string-interpolation": "npm:^0.5.7"
+ "@graphql-mesh/transport-common": "npm:^0.7.25"
"@graphql-mesh/transport-soap": "npm:^0.8.10"
"@graphql-mesh/types": "npm:^0.103.10"
"@graphql-mesh/utils": "npm:^0.103.10"