From 33699e7f0fc4d4f365bbb252a4b5c00bf5b3b36c Mon Sep 17 00:00:00 2001 From: Clement Habinshuti Date: Wed, 14 Jul 2021 14:51:07 +0300 Subject: [PATCH 1/6] Refactor handleResponseText in ODataHandler --- packages/handlers/odata/src/index.ts | 201 +----------------- packages/handlers/odata/src/query-options.ts | 30 +++ .../handlers/odata/src/request-processing.ts | 96 +++++++++ packages/handlers/odata/src/scalars.ts | 20 ++ packages/handlers/odata/src/schema-util.ts | 13 ++ packages/handlers/odata/src/util.ts | 8 + 6 files changed, 172 insertions(+), 196 deletions(-) create mode 100644 packages/handlers/odata/src/query-options.ts create mode 100644 packages/handlers/odata/src/request-processing.ts create mode 100644 packages/handlers/odata/src/scalars.ts create mode 100644 packages/handlers/odata/src/schema-util.ts create mode 100644 packages/handlers/odata/src/util.ts diff --git a/packages/handlers/odata/src/index.ts b/packages/handlers/odata/src/index.ts index 6483daa36e4ba..aae526479a105 100644 --- a/packages/handlers/odata/src/index.ts +++ b/packages/handlers/odata/src/index.ts @@ -36,8 +36,6 @@ import { GraphQLISO8601Duration, } from 'graphql-scalars'; import { - isListType, - GraphQLResolveInfo, isAbstractType, GraphQLObjectType, GraphQLSchema, @@ -54,70 +52,12 @@ import { pruneSchema } from '@graphql-tools/utils'; import { Request, Response } from 'cross-fetch'; import { PredefinedProxyOptions } from '@graphql-mesh/store'; import { env } from 'process'; +import { SCALARS } from './scalars'; +import { queryOptionsFields } from './query-options'; +import { getUrlString, addIdentifierToUrl } from './util'; +import { EntityTypeExtensions } from './schema-util'; +import { handleResponseText } from './request-processing'; -const SCALARS = new Map([ - ['Edm.Binary', 'String'], - ['Edm.Stream', 'String'], - ['Edm.String', 'String'], - ['Edm.Int16', 'Int'], - ['Edm.Byte', 'Byte'], - ['Edm.Int32', 'Int'], - ['Edm.Int64', 'BigInt'], - ['Edm.Double', 'Float'], - ['Edm.Boolean', 'Boolean'], - ['Edm.Guid', 'GUID'], - ['Edm.DateTimeOffset', 'DateTime'], - ['Edm.Date', 'Date'], - ['Edm.TimeOfDay', 'String'], - ['Edm.Single', 'Float'], - ['Edm.Duration', 'ISO8601Duration'], - ['Edm.Decimal', 'Float'], - ['Edm.SByte', 'Byte'], - ['Edm.GeographyPoint', 'String'], -]); - -interface EntityTypeExtensions { - entityInfo: { - actualFields: string[]; - navigationFields: string[]; - identifierFieldName?: string; - identifierFieldTypeRef?: string; - isOpenType: boolean; - }; - typeObj: any; - eventEmitter: EventEmitter; -} - -const queryOptionsFields = { - orderby: { - type: 'String', - description: - 'A data service URI with a $orderby System Query Option specifies an expression for determining what values are used to order the collection of Entries identified by the Resource Path section of the URI. This query option is only supported when the resource path identifies a Collection of Entries.', - }, - top: { - type: 'Int', - description: - 'A data service URI with a $top System Query Option identifies a subset of the Entries in the Collection of Entries identified by the Resource Path section of the URI. This subset is formed by selecting only the first N items of the set, where N is an integer greater than or equal to zero specified by this query option. If a value less than zero is specified, the URI should be considered malformed.', - }, - skip: { - type: 'Int', - description: - 'A data service URI with a $skip System Query Option identifies a subset of the Entries in the Collection of Entries identified by the Resource Path section of the URI. That subset is defined by seeking N Entries into the Collection and selecting only the remaining Entries (starting with Entry N+1). N is an integer greater than or equal to zero specified by this query option. If a value less than zero is specified, the URI should be considered malformed.', - }, - filter: { - type: 'String', - description: - 'A URI with a $filter System Query Option identifies a subset of the Entries from the Collection of Entries identified by the Resource Path section of the URI. The subset is determined by selecting only the Entries that satisfy the predicate expression specified by the query option.', - }, - inlinecount: { - type: 'InlineCount', - description: - 'A URI with a $inlinecount System Query Option specifies that the response to the request includes a count of the number of Entries in the Collection of Entries identified by the Resource Path section of the URI. The count must be calculated after applying any $filter System Query Options present in the URI. The set of valid values for the $inlinecount query option are shown in the table below. If a value other than one shown in Table 4 is specified the URI is considered malformed.', - }, - count: { - type: 'Boolean', - }, -}; export default class ODataHandler implements MeshHandler { private name: string; @@ -251,133 +191,6 @@ export default class ODataHandler implements MeshHandler { return realTypeName; } - function getUrlString(url: URL) { - return decodeURIComponent(url.toString()).split('+').join(' '); - } - - function handleResponseText(responseText: string, urlString: string, info: GraphQLResolveInfo) { - let responseJson: any; - try { - responseJson = JSON.parse(responseText); - } catch (error) { - const actualError = new Error(responseText); - Object.assign(actualError, { - extensions: { - url: urlString, - }, - }); - throw actualError; - } - if (responseJson.error) { - const actualError = new Error(responseJson.error.message || responseJson.error) as any; - actualError.extensions = responseJson.error; - throw actualError; - } - const urlStringWithoutSearchParams = urlString.split('?')[0]; - if (isListType(info.returnType)) { - const actualReturnType: GraphQLObjectType = info.returnType.ofType; - const entityTypeExtensions = actualReturnType.extensions as EntityTypeExtensions; - if ('Message' in responseJson && !('value' in responseJson)) { - const error = new Error(responseJson.Message); - Object.assign(error, { extensions: responseJson }); - throw error; - } - const returnList: any[] = responseJson.value; - return returnList.map(element => { - if (!entityTypeExtensions?.entityInfo) { - return element; - } - const urlOfElement = new URL(urlStringWithoutSearchParams); - addIdentifierToUrl( - urlOfElement, - entityTypeExtensions.entityInfo.identifierFieldName, - entityTypeExtensions.entityInfo.identifierFieldTypeRef, - element - ); - const identifierUrl = element['@odata.id'] || getUrlString(urlOfElement); - const fieldMap = actualReturnType.getFields(); - for (const fieldName in element) { - if (entityTypeExtensions.entityInfo.navigationFields.includes(fieldName)) { - const field = element[fieldName]; - let fieldType = fieldMap[fieldName].type; - if ('ofType' in fieldType) { - fieldType = fieldType.ofType; - } - const { entityInfo: fieldEntityInfo } = (fieldType as any).extensions as EntityTypeExtensions; - if (field instanceof Array) { - for (const fieldElement of field) { - const urlOfField = new URL(urljoin(identifierUrl, fieldName)); - addIdentifierToUrl( - urlOfField, - fieldEntityInfo.identifierFieldName, - fieldEntityInfo.identifierFieldTypeRef, - fieldElement - ); - fieldElement['@odata.id'] = fieldElement['@odata.id'] || getUrlString(urlOfField); - } - } else { - const urlOfField = new URL(urljoin(identifierUrl, fieldName)); - addIdentifierToUrl( - urlOfField, - fieldEntityInfo.identifierFieldName, - fieldEntityInfo.identifierFieldTypeRef, - field - ); - field['@odata.id'] = field['@odata.id'] || getUrlString(urlOfField); - } - } - } - return { - '@odata.id': identifierUrl, - ...element, - }; - }); - } else { - const actualReturnType = info.returnType as GraphQLObjectType; - const entityTypeExtensions = actualReturnType.extensions as EntityTypeExtensions; - if (!entityTypeExtensions?.entityInfo) { - return responseJson; - } - const identifierUrl = responseJson['@odata.id'] || urlStringWithoutSearchParams; - const fieldMap = actualReturnType.getFields(); - for (const fieldName in responseJson) { - if (entityTypeExtensions?.entityInfo.navigationFields.includes(fieldName)) { - const field = responseJson[fieldName]; - let fieldType = fieldMap[fieldName].type; - if ('ofType' in fieldType) { - fieldType = fieldType.ofType; - } - const { entityInfo: fieldEntityInfo } = (fieldType as any).extensions as EntityTypeExtensions; - if (field instanceof Array) { - for (const fieldElement of field) { - const urlOfField = new URL(urljoin(identifierUrl, fieldName)); - addIdentifierToUrl( - urlOfField, - fieldEntityInfo.identifierFieldName, - fieldEntityInfo.identifierFieldTypeRef, - fieldElement - ); - fieldElement['@odata.id'] = fieldElement['@odata.id'] || getUrlString(urlOfField); - } - } else { - const urlOfField = new URL(urljoin(identifierUrl, fieldName)); - addIdentifierToUrl( - urlOfField, - fieldEntityInfo.identifierFieldName, - fieldEntityInfo.identifierFieldTypeRef, - field - ); - field['@odata.id'] = field['@odata.id'] || getUrlString(urlOfField); - } - } - } - return { - '@odata.id': responseJson['@odata.id'] || urlStringWithoutSearchParams, - ...responseJson, - }; - } - } - schemaComposer.createEnumTC({ name: 'InlineCount', values: { @@ -424,10 +237,6 @@ export default class ODataHandler implements MeshHandler { return null; } - function addIdentifierToUrl(url: URL, identifierFieldName: string, identifierFieldTypeRef: string, args: any) { - url.href += `/${args[identifierFieldName]}/`; - } - function rebuildOpenInputObjects(input: any) { if (typeof input === 'object') { if ('rest' in input) { diff --git a/packages/handlers/odata/src/query-options.ts b/packages/handlers/odata/src/query-options.ts new file mode 100644 index 0000000000000..0d025c2a559e8 --- /dev/null +++ b/packages/handlers/odata/src/query-options.ts @@ -0,0 +1,30 @@ +export const queryOptionsFields = { + orderby: { + type: 'String', + description: + 'A data service URI with a $orderby System Query Option specifies an expression for determining what values are used to order the collection of Entries identified by the Resource Path section of the URI. This query option is only supported when the resource path identifies a Collection of Entries.', + }, + top: { + type: 'Int', + description: + 'A data service URI with a $top System Query Option identifies a subset of the Entries in the Collection of Entries identified by the Resource Path section of the URI. This subset is formed by selecting only the first N items of the set, where N is an integer greater than or equal to zero specified by this query option. If a value less than zero is specified, the URI should be considered malformed.', + }, + skip: { + type: 'Int', + description: + 'A data service URI with a $skip System Query Option identifies a subset of the Entries in the Collection of Entries identified by the Resource Path section of the URI. That subset is defined by seeking N Entries into the Collection and selecting only the remaining Entries (starting with Entry N+1). N is an integer greater than or equal to zero specified by this query option. If a value less than zero is specified, the URI should be considered malformed.', + }, + filter: { + type: 'String', + description: + 'A URI with a $filter System Query Option identifies a subset of the Entries from the Collection of Entries identified by the Resource Path section of the URI. The subset is determined by selecting only the Entries that satisfy the predicate expression specified by the query option.', + }, + inlinecount: { + type: 'InlineCount', + description: + 'A URI with a $inlinecount System Query Option specifies that the response to the request includes a count of the number of Entries in the Collection of Entries identified by the Resource Path section of the URI. The count must be calculated after applying any $filter System Query Options present in the URI. The set of valid values for the $inlinecount query option are shown in the table below. If a value other than one shown in Table 4 is specified the URI is considered malformed.', + }, + count: { + type: 'Boolean', + }, +}; diff --git a/packages/handlers/odata/src/request-processing.ts b/packages/handlers/odata/src/request-processing.ts new file mode 100644 index 0000000000000..5b99b7584b1bb --- /dev/null +++ b/packages/handlers/odata/src/request-processing.ts @@ -0,0 +1,96 @@ +import { isListType, GraphQLObjectType, GraphQLResolveInfo } from "graphql"; +import { EntityTypeExtensions } from "./schema-util"; +import { getUrlString, addIdentifierToUrl } from "./util"; +import urljoin from 'url-join'; + +export function handleResponseText(responseText: string, urlString: string, info: GraphQLResolveInfo) { + let responseJson: any; + try { + responseJson = JSON.parse(responseText); + } catch (error) { + const actualError = new Error(responseText); + Object.assign(actualError, { + extensions: { + url: urlString, + }, + }); + throw actualError; + } + if (responseJson.error) { + const actualError = new Error(responseJson.error.message || responseJson.error) as any; + actualError.extensions = responseJson.error; + throw actualError; + } + const urlStringWithoutSearchParams = urlString.split('?')[0]; + if (isListType(info.returnType)) { + const actualReturnType: GraphQLObjectType = info.returnType.ofType; + const entityTypeExtensions = actualReturnType.extensions as EntityTypeExtensions; + if ('Message' in responseJson && !('value' in responseJson)) { + const error = new Error(responseJson.Message); + Object.assign(error, { extensions: responseJson }); + throw error; + } + const returnList: any[] = responseJson.value; + return returnList.map(element => { + const urlOfElement = new URL(urlStringWithoutSearchParams); + addIdentifierToUrl( + urlOfElement, + entityTypeExtensions.entityInfo.identifierFieldName, + entityTypeExtensions.entityInfo.identifierFieldTypeRef, + element + ); + const identifierUrl = element['@odata.id'] || getUrlString(urlOfElement); + return buildResponseObject(element, actualReturnType, identifierUrl); + }); + } else { + const actualReturnType = info.returnType as GraphQLObjectType; + const identifierUrl = responseJson['@odata.id'] || urlStringWithoutSearchParams; + + return buildResponseObject(responseJson, actualReturnType, identifierUrl); + } +} + +function buildResponseObject(originalObject: any, actualReturnType: GraphQLObjectType, identifierUrl: any) { + const entityTypeExtensions = actualReturnType.extensions as EntityTypeExtensions; + if (!entityTypeExtensions?.entityInfo) { + return originalObject; + } + + const fieldMap = actualReturnType.getFields(); + for (const fieldName in originalObject) { + if (entityTypeExtensions?.entityInfo.navigationFields.includes(fieldName)) { + const field = originalObject[fieldName]; + let fieldType = fieldMap[fieldName].type; + if ('ofType' in fieldType) { + fieldType = fieldType.ofType; + } + const { entityInfo: fieldEntityInfo } = (fieldType as any).extensions as EntityTypeExtensions; + if (field instanceof Array) { + for (const fieldElement of field) { + const urlOfField = new URL(urljoin(identifierUrl, fieldName)); + addIdentifierToUrl( + urlOfField, + fieldEntityInfo.identifierFieldName, + fieldEntityInfo.identifierFieldTypeRef, + fieldElement + ); + fieldElement['@odata.id'] = field['@odata.id'] || getUrlString(urlOfField); + } + } else { + const urlOfField = new URL(urljoin(identifierUrl, fieldName)); + addIdentifierToUrl( + urlOfField, + fieldEntityInfo.identifierFieldName, + fieldEntityInfo.identifierFieldTypeRef, + field + ); + field['@odata.id'] = field['@odata.id'] || getUrlString(urlOfField); + } + } + } + + return { + '@odata.id': identifierUrl, + ...originalObject, + }; +} diff --git a/packages/handlers/odata/src/scalars.ts b/packages/handlers/odata/src/scalars.ts new file mode 100644 index 0000000000000..91e66e7cc6388 --- /dev/null +++ b/packages/handlers/odata/src/scalars.ts @@ -0,0 +1,20 @@ +export const SCALARS = new Map([ + ['Edm.Binary', 'String'], + ['Edm.Stream', 'String'], + ['Edm.String', 'String'], + ['Edm.Int16', 'Int'], + ['Edm.Byte', 'Byte'], + ['Edm.Int32', 'Int'], + ['Edm.Int64', 'BigInt'], + ['Edm.Double', 'Float'], + ['Edm.Boolean', 'Boolean'], + ['Edm.Guid', 'GUID'], + ['Edm.DateTimeOffset', 'DateTime'], + ['Edm.Date', 'Date'], + ['Edm.TimeOfDay', 'String'], + ['Edm.Single', 'Float'], + ['Edm.Duration', 'ISO8601Duration'], + ['Edm.Decimal', 'Float'], + ['Edm.SByte', 'Byte'], + ['Edm.GeographyPoint', 'String'], +]); diff --git a/packages/handlers/odata/src/schema-util.ts b/packages/handlers/odata/src/schema-util.ts new file mode 100644 index 0000000000000..6e3831deaf240 --- /dev/null +++ b/packages/handlers/odata/src/schema-util.ts @@ -0,0 +1,13 @@ +import { EventEmitter } from 'events'; + +export interface EntityTypeExtensions { + entityInfo: { + actualFields: string[]; + navigationFields: string[]; + identifierFieldName?: string; + identifierFieldTypeRef?: string; + isOpenType: boolean; + }; + typeObj: any; + eventEmitter: EventEmitter; +} diff --git a/packages/handlers/odata/src/util.ts b/packages/handlers/odata/src/util.ts new file mode 100644 index 0000000000000..01a57aba472bf --- /dev/null +++ b/packages/handlers/odata/src/util.ts @@ -0,0 +1,8 @@ + +export function getUrlString(url: URL) { + return decodeURIComponent(url.toString()).split('+').join(' '); +} + +export function addIdentifierToUrl(url: URL, identifierFieldName: string, identifierFieldTypeRef: string, args: any) { + url.href += `/${args[identifierFieldName]}/`; +} From 1fc27fc289e71ac0f7c612b3e0a5b1a5d4a0f78a Mon Sep 17 00:00:00 2001 From: Clement Habinshuti Date: Wed, 14 Jul 2021 17:40:42 +0300 Subject: [PATCH 2/6] Refactor OData handler data loader --- packages/handlers/odata/src/index.ts | 118 +--------------- .../handlers/odata/src/request-processing.ts | 131 ++++++++++++++++++ packages/handlers/odata/src/util.ts | 1 + 3 files changed, 134 insertions(+), 116 deletions(-) diff --git a/packages/handlers/odata/src/index.ts b/packages/handlers/odata/src/index.ts index aae526479a105..946101ba06f7a 100644 --- a/packages/handlers/odata/src/index.ts +++ b/packages/handlers/odata/src/index.ts @@ -56,7 +56,7 @@ import { SCALARS } from './scalars'; import { queryOptionsFields } from './query-options'; import { getUrlString, addIdentifierToUrl } from './util'; import { EntityTypeExtensions } from './schema-util'; -import { handleResponseText } from './request-processing'; +import { handleBatchJsonResults, handleResponseText, getDataLoaderFactory } from './request-processing'; export default class ODataHandler implements MeshHandler { @@ -249,121 +249,7 @@ export default class ODataHandler implements MeshHandler { } } - function handleBatchJsonResults(batchResponseJson: any, requests: Request[]) { - if ('error' in batchResponseJson) { - const error = new Error(batchResponseJson.error.message); - Object.assign(error, { - extensions: batchResponseJson.error, - }); - throw error; - } - if (!('responses' in batchResponseJson)) { - const error = new Error(`Batch Request didn't return a valid response.`); - Object.assign(error, { - extensions: batchResponseJson, - }); - throw error; - } - return requests.map((_req, index) => { - const responseObj = batchResponseJson.responses.find((res: any) => res.id === index.toString()); - return new Response(jsonFlatStringify(responseObj.body), { - status: responseObj.status, - headers: responseObj.headers, - }); - }); - } - - const DATALOADER_FACTORIES = { - multipart: (context: any) => - new DataLoader(async (requests: Request[]): Promise => { - let requestBody = ''; - const requestBoundary = 'batch_' + Date.now(); - for (const requestIndex in requests) { - requestBody += `--${requestBoundary}\n`; - const request = requests[requestIndex]; - requestBody += `Content-Type: application/http\n`; - requestBody += `Content-Transfer-Encoding:binary\n`; - requestBody += `Content-ID: ${requestIndex}\n\n`; - requestBody += `${request.method} ${request.url} HTTP/1.1\n`; - request.headers?.forEach((value, key) => { - requestBody += `${key}: ${value}\n`; - }); - if (request.body) { - const bodyAsStr = await request.text(); - requestBody += `Content-Length: ${bodyAsStr.length}`; - requestBody += `\n`; - requestBody += bodyAsStr; - } - requestBody += `\n`; - } - requestBody += `--${requestBoundary}--\n`; - const batchHeaders = headersFactory({ context, env }, 'POST'); - batchHeaders.set('Content-Type', `multipart/mixed;boundary=${requestBoundary}`); - const batchRequest = new Request(urljoin(baseUrl, '$batch'), { - method: 'POST', - body: requestBody, - headers: batchHeaders, - }); - const batchResponse = await nativeFetch(batchRequest); - const batchResponseText = await batchResponse.text(); - if (!batchResponseText.startsWith('--')) { - const batchResponseJson = JSON.parse(batchResponseText); - return handleBatchJsonResults(batchResponseJson, requests); - } - const responseLines = batchResponseText.split('\n'); - const responseBoundary = responseLines[0]; - const actualResponse = responseLines.slice(1, responseLines.length - 2).join('\n'); - const responseTextArr = actualResponse.split(responseBoundary); - return responseTextArr.map(responseTextWithContentHeader => { - const responseText = responseTextWithContentHeader.split('\n').slice(4).join('\n'); - const { body, headers, statusCode, statusMessage } = parseResponse(responseText); - return new Response(body, { - headers, - status: parseInt(statusCode), - statusText: statusMessage, - }); - }); - }), - json: (context: any) => - new DataLoader(async (requests: Request[]): Promise => { - const batchHeaders = headersFactory({ context, env }, 'POST'); - batchHeaders.set('Content-Type', 'application/json'); - const batchRequest = new Request(urljoin(baseUrl, '$batch'), { - method: 'POST', - body: jsonFlatStringify({ - requests: await Promise.all( - requests.map(async (request, index) => { - const id = index.toString(); - const url = request.url.replace(baseUrl, ''); - const method = request.method; - const headers: HeadersInit = {}; - request.headers?.forEach((value, key) => { - headers[key] = value; - }); - return { - id, - url, - method, - body: request.body && (await request.json()), - headers, - }; - }) - ), - }), - headers: batchHeaders, - }); - const batchResponse = await fetch(batchRequest); - const batchResponseText = await batchResponse.text(); - const batchResponseJson = JSON.parse(batchResponseText); - return handleBatchJsonResults(batchResponseJson, requests); - }), - none: () => - new DataLoader( - (requests: Request[]): Promise => Promise.all(requests.map(request => fetch(request))) - ), - }; - - const dataLoaderFactory = DATALOADER_FACTORIES[this.config.batch || 'none']; + const dataLoaderFactory = getDataLoaderFactory(this.config.batch || 'none', baseUrl, env, headersFactory, fetch); function buildName({ schemaNamespace, name }: { schemaNamespace: string; name: string }) { const alias = aliasNamespaceMap.get(schemaNamespace) || schemaNamespace; diff --git a/packages/handlers/odata/src/request-processing.ts b/packages/handlers/odata/src/request-processing.ts index 5b99b7584b1bb..3b9e56608d725 100644 --- a/packages/handlers/odata/src/request-processing.ts +++ b/packages/handlers/odata/src/request-processing.ts @@ -1,7 +1,138 @@ import { isListType, GraphQLObjectType, GraphQLResolveInfo } from "graphql"; +import DataLoader from 'dataloader'; +import { parseResponse } from 'http-string-parser'; +import { getCachedFetch, jsonFlatStringify } from "@graphql-mesh/utils"; +import { Request, Response } from 'cross-fetch'; +import { nativeFetch } from './native-fetch'; import { EntityTypeExtensions } from "./schema-util"; import { getUrlString, addIdentifierToUrl } from "./util"; import urljoin from 'url-join'; +import { ResolverData } from "@graphql-mesh/types"; + +type HeadersFactory = (resolverData: ResolverData, method: string) => Headers; +type DataLoaderFactory = (context: any) => DataLoader; +type DataLoaderType = 'multipart' | 'json' | 'none'; + +export function getDataLoaderFactory(type: DataLoaderType, baseUrl: string, env: Record, headersFactory: HeadersFactory, fetch: ReturnType): DataLoaderFactory { + const factories = initDataloaderFactories(baseUrl, env, headersFactory, fetch); + return factories[type]; +} + +function initDataloaderFactories(baseUrl: string, env: Record, headersFactory: HeadersFactory, fetch: ReturnType) { + return { + multipart: (context: any) => + new DataLoader(async (requests: Request[]): Promise => { + let requestBody = ''; + const requestBoundary = 'batch_' + Date.now(); + for (const requestIndex in requests) { + requestBody += `--${requestBoundary}\n`; + const request = requests[requestIndex]; + requestBody += `Content-Type: application/http\n`; + requestBody += `Content-Transfer-Encoding:binary\n`; + requestBody += `Content-ID: ${requestIndex}\n\n`; + requestBody += `${request.method} ${request.url} HTTP/1.1\n`; + request.headers?.forEach((value, key) => { + requestBody += `${key}: ${value}\n`; + }); + if (request.body) { + const bodyAsStr = await request.text(); + requestBody += `Content-Length: ${bodyAsStr.length}`; + requestBody += `\n`; + requestBody += bodyAsStr; + } + requestBody += `\n`; + } + requestBody += `--${requestBoundary}--\n`; + const batchHeaders = headersFactory({ context, env }, 'POST'); + batchHeaders.set('Content-Type', `multipart/mixed;boundary=${requestBoundary}`); + const batchRequest = new Request(urljoin(baseUrl, '$batch'), { + method: 'POST', + body: requestBody, + headers: batchHeaders, + }); + const batchResponse = await nativeFetch(batchRequest); + const batchResponseText = await batchResponse.text(); + if (!batchResponseText.startsWith('--')) { + const batchResponseJson = JSON.parse(batchResponseText); + return handleBatchJsonResults(batchResponseJson, requests); + } + const responseLines = batchResponseText.split('\n'); + const responseBoundary = responseLines[0]; + const actualResponse = responseLines.slice(1, responseLines.length - 2).join('\n'); + const responseTextArr = actualResponse.split(responseBoundary); + return responseTextArr.map(responseTextWithContentHeader => { + const responseText = responseTextWithContentHeader.split('\n').slice(4).join('\n'); + const { body, headers, statusCode, statusMessage } = parseResponse(responseText); + return new Response(body, { + headers, + status: parseInt(statusCode), + statusText: statusMessage, + }); + }); + }), + json: (context: any) => + new DataLoader(async (requests: Request[]): Promise => { + const batchHeaders = headersFactory({ context, env }, 'POST'); + batchHeaders.set('Content-Type', 'application/json'); + const batchRequest = new Request(urljoin(baseUrl, '$batch'), { + method: 'POST', + body: jsonFlatStringify({ + requests: await Promise.all( + requests.map(async (request, index) => { + const id = index.toString(); + const url = request.url.replace(baseUrl, ''); + const method = request.method; + const headers: HeadersInit = {}; + request.headers?.forEach((value, key) => { + headers[key] = value; + }); + return { + id, + url, + method, + body: request.body && (await request.json()), + headers, + }; + }) + ), + }), + headers: batchHeaders, + }); + const batchResponse = await fetch(batchRequest); + const batchResponseText = await batchResponse.text(); + const batchResponseJson = JSON.parse(batchResponseText); + return handleBatchJsonResults(batchResponseJson, requests); + }), + none: () => + new DataLoader( + (requests: Request[]): Promise => Promise.all(requests.map(request => fetch(request))) + ), + }; +}; + +export function handleBatchJsonResults(batchResponseJson: any, requests: Request[]) { + if ('error' in batchResponseJson) { + const error = new Error(batchResponseJson.error.message); + Object.assign(error, { + extensions: batchResponseJson.error, + }); + throw error; + } + if (!('responses' in batchResponseJson)) { + const error = new Error(`Batch Request didn't return a valid response.`); + Object.assign(error, { + extensions: batchResponseJson, + }); + throw error; + } + return requests.map((_req, index) => { + const responseObj = batchResponseJson.responses.find((res: any) => res.id === index.toString()); + return new Response(jsonFlatStringify(responseObj.body), { + status: responseObj.status, + headers: responseObj.headers, + }); + }); +} export function handleResponseText(responseText: string, urlString: string, info: GraphQLResolveInfo) { let responseJson: any; diff --git a/packages/handlers/odata/src/util.ts b/packages/handlers/odata/src/util.ts index 01a57aba472bf..7f2a6984ca3ea 100644 --- a/packages/handlers/odata/src/util.ts +++ b/packages/handlers/odata/src/util.ts @@ -1,3 +1,4 @@ +import { ResolverData } from "@graphql-mesh/types"; export function getUrlString(url: URL) { return decodeURIComponent(url.toString()).split('+').join(' '); From 6cf12380e126e24d45c79a376e57de2d3d60d3c3 Mon Sep 17 00:00:00 2001 From: Clement Habinshuti Date: Thu, 15 Jul 2021 12:01:48 +0300 Subject: [PATCH 3/6] Move OData handler schema composition to separate file --- packages/handlers/odata/src/index.ts | 991 +--------------- .../handlers/odata/src/request-processing.ts | 2 +- .../handlers/odata/src/schema-generation.ts | 1016 +++++++++++++++++ 3 files changed, 1029 insertions(+), 980 deletions(-) create mode 100644 packages/handlers/odata/src/schema-generation.ts diff --git a/packages/handlers/odata/src/index.ts b/packages/handlers/odata/src/index.ts index 946101ba06f7a..d7c01e99b83d6 100644 --- a/packages/handlers/odata/src/index.ts +++ b/packages/handlers/odata/src/index.ts @@ -11,52 +11,18 @@ import { parseInterpolationStrings, getInterpolatedHeadersFactory, readFileOrUrlWithCache, - jsonFlatStringify, getCachedFetch, loadFromModuleExportExpression, stringInterpolator, } from '@graphql-mesh/utils'; import urljoin from 'url-join'; -import { - SchemaComposer, - ObjectTypeComposer, - InterfaceTypeComposer, - ObjectTypeComposerFieldConfigDefinition, - ObjectTypeComposerArgumentConfigMapDefinition, - EnumTypeComposerValueConfigDefinition, - InputTypeComposer, -} from 'graphql-compose'; -import { - GraphQLBigInt, - GraphQLGUID, - GraphQLDateTime, - GraphQLJSON, - GraphQLDate, - GraphQLByte, - GraphQLISO8601Duration, -} from 'graphql-scalars'; -import { - isAbstractType, - GraphQLObjectType, - GraphQLSchema, - specifiedDirectives, -} from 'graphql'; -import { parseResolveInfo, ResolveTree, simplifyParsedResolveInfoFragmentWithType } from 'graphql-parse-resolve-info'; -import DataLoader from 'dataloader'; -import { parseResponse } from 'http-string-parser'; -import { nativeFetch } from './native-fetch'; -import { pascalCase } from 'pascal-case'; import { EventEmitter } from 'events'; import { parse as parseXML } from 'fast-xml-parser'; import { pruneSchema } from '@graphql-tools/utils'; -import { Request, Response } from 'cross-fetch'; import { PredefinedProxyOptions } from '@graphql-mesh/store'; import { env } from 'process'; -import { SCALARS } from './scalars'; -import { queryOptionsFields } from './query-options'; -import { getUrlString, addIdentifierToUrl } from './util'; -import { EntityTypeExtensions } from './schema-util'; -import { handleBatchJsonResults, handleResponseText, getDataLoaderFactory } from './request-processing'; +import { getDataLoaderFactory } from './request-processing'; +import { generateGraphQLSchema } from "./schema-generation"; export default class ODataHandler implements MeshHandler { @@ -122,96 +88,9 @@ export default class ODataHandler implements MeshHandler { env, }); - const schemaComposer = new SchemaComposer(); - schemaComposer.add(GraphQLBigInt); - schemaComposer.add(GraphQLGUID); - schemaComposer.add(GraphQLDateTime); - schemaComposer.add(GraphQLJSON); - schemaComposer.add(GraphQLByte); - schemaComposer.add(GraphQLDate); - schemaComposer.add(GraphQLISO8601Duration); - - const aliasNamespaceMap = new Map(); - const metadataJson = await this.getCachedMetadataJson(fetch); - const schemas = metadataJson.Edmx[0].DataServices[0].Schema; - const multipleSchemas = schemas.length > 1; - const namespaces = new Set(); - const contextDataloaderName = Symbol(`${this.name}DataLoader`); - function getNamespaceFromTypeRef(typeRef: string) { - let namespace = ''; - namespaces?.forEach(el => { - if ( - typeRef.startsWith(el) && - el.length > namespace.length && // It can be deeper namespace - !typeRef.replace(el + '.', '').includes('.') // Typename cannot have `.` - ) { - namespace = el; - } - }); - return namespace; - } - - function getTypeNameFromRef({ - typeRef, - isInput, - isRequired, - }: { - typeRef: string; - isInput: boolean; - isRequired: boolean; - }) { - const typeRefArr = typeRef.split('Collection('); - const arrayDepth = typeRefArr.length; - let actualTypeRef = typeRefArr.join('').split(')').join(''); - const typeNamespace = getNamespaceFromTypeRef(actualTypeRef); - if (aliasNamespaceMap.has(typeNamespace)) { - const alias = aliasNamespaceMap.get(typeNamespace); - actualTypeRef = actualTypeRef.replace(typeNamespace, alias); - } - const actualTypeRefArr = actualTypeRef.split('.'); - const typeName = multipleSchemas - ? pascalCase(actualTypeRefArr.join('_')) - : actualTypeRefArr[actualTypeRefArr.length - 1]; - let realTypeName = typeName; - if (SCALARS.has(actualTypeRef)) { - realTypeName = SCALARS.get(actualTypeRef); - } else if (schemaComposer.isEnumType(typeName)) { - realTypeName = typeName; - } else if (isInput) { - realTypeName += 'Input'; - } - const fakeEmptyArr = new Array(arrayDepth); - realTypeName = fakeEmptyArr.join('[') + realTypeName + fakeEmptyArr.join(']'); - if (isRequired) { - realTypeName += '!'; - } - return realTypeName; - } - - schemaComposer.createEnumTC({ - name: 'InlineCount', - values: { - allpages: { - value: 'allpages', - description: - 'The OData MUST include a count of the number of entities in the collection identified by the URI (after applying any $filter System Query Options present on the URI)', - }, - none: { - value: 'none', - description: - 'The OData service MUST NOT include a count in the response. This is equivalence to a URI that does not include a $inlinecount query string parameter.', - }, - }, - }); - - schemaComposer.createInputTC({ - name: 'QueryOptions', - fields: queryOptionsFields, - }); - const origHeadersFactory = getInterpolatedHeadersFactory(operationHeaders); const headersFactory = (resolverData: ResolverData, method: string) => { const headers = origHeadersFactory(resolverData); @@ -228,820 +107,20 @@ export default class ODataHandler implements MeshHandler { baseUrl, ]); - function getTCByTypeNames(...typeNames: string[]) { - for (const typeName of typeNames) { - try { - return schemaComposer.getAnyTC(typeName); - } catch {} - } - return null; - } - - function rebuildOpenInputObjects(input: any) { - if (typeof input === 'object') { - if ('rest' in input) { - Object.assign(input, input.rest); - delete input.rest; - } - for (const fieldName in input) { - rebuildOpenInputObjects(input[fieldName]); - } - } - } - const dataLoaderFactory = getDataLoaderFactory(this.config.batch || 'none', baseUrl, env, headersFactory, fetch); - function buildName({ schemaNamespace, name }: { schemaNamespace: string; name: string }) { - const alias = aliasNamespaceMap.get(schemaNamespace) || schemaNamespace; - const ref = alias + '.' + name; - return multipleSchemas ? pascalCase(ref.split('.').join('_')) : name; - } - - schemas?.forEach((schemaObj: any) => { - const schemaNamespace = schemaObj.attributes.Namespace; - namespaces.add(schemaNamespace); - const schemaAlias = schemaObj.attributes.Alias; - if (schemaAlias) { - aliasNamespaceMap.set(schemaNamespace, schemaAlias); - } - }); - - schemas?.forEach((schemaObj: any) => { - const schemaNamespace = schemaObj.attributes.Namespace; - - schemaObj.EnumType?.forEach((enumObj: any) => { - const values: Record = {}; - enumObj.Member?.forEach((memberObj: any) => { - const key = memberObj.attributes.Name; - // This doesn't work. - // const value = memberElement.getAttribute('Value')!; - values[key] = { - value: key, - extensions: { memberObj }, - }; - }); - const enumTypeName = buildName({ schemaNamespace, name: enumObj.attributes.Name }); - schemaComposer.createEnumTC({ - name: enumTypeName, - values, - extensions: { enumObj }, - }); - }); - - const allTypes = (schemaObj.EntityType || []).concat(schemaObj.ComplexType || []); - const typesWithBaseType = allTypes.filter((typeObj: any) => typeObj.attributes.BaseType); - - allTypes?.forEach((typeObj: any) => { - const entityTypeName = buildName({ schemaNamespace, name: typeObj.attributes.Name }); - const isOpenType = typeObj.attributes.OpenType === 'true'; - const isAbstract = typeObj.attributes.Abstract === 'true'; - const eventEmitter = new EventEmitter(); - eventEmitter.setMaxListeners(Infinity); - this.eventEmitterSet.add(eventEmitter); - const extensions: EntityTypeExtensions = { - entityInfo: { - actualFields: [], - navigationFields: [], - isOpenType, - }, - typeObj, - eventEmitter, - }; - const inputType = schemaComposer.createInputTC({ - name: entityTypeName + 'Input', - fields: {}, - extensions: () => extensions, - }); - let abstractType: InterfaceTypeComposer; - if ( - typesWithBaseType.some((typeObj: any) => typeObj.attributes.BaseType.includes(`.${entityTypeName}`)) || - isAbstract - ) { - abstractType = schemaComposer.createInterfaceTC({ - name: isAbstract ? entityTypeName : `I${entityTypeName}`, - extensions, - resolveType: (root: any) => { - const typeRef = root['@odata.type']?.replace('#', ''); - if (typeRef) { - const typeName = getTypeNameFromRef({ - typeRef: root['@odata.type'].replace('#', ''), - isInput: false, - isRequired: false, - }); - return typeName; - } - return isAbstract ? `T${entityTypeName}` : entityTypeName; - }, - }); - } - const outputType = schemaComposer.createObjectTC({ - name: isAbstract ? `T${entityTypeName}` : entityTypeName, - extensions, - interfaces: abstractType ? [abstractType] : [], - }); - - abstractType?.setInputTypeComposer(inputType); - outputType.setInputTypeComposer(inputType); - - const propertyRefObj = typeObj.Key && typeObj.Key[0].PropertyRef[0]; - if (propertyRefObj) { - extensions.entityInfo.identifierFieldName = propertyRefObj.attributes.Name; - } - - typeObj.Property?.forEach((propertyObj: any) => { - const propertyName = propertyObj.attributes.Name; - extensions.entityInfo.actualFields.push(propertyName); - const propertyTypeRef = propertyObj.attributes.Type; - if (propertyName === extensions.entityInfo.identifierFieldName) { - extensions.entityInfo.identifierFieldTypeRef = propertyTypeRef; - } - const isRequired = propertyObj.attributes.Nullable === 'false'; - inputType.addFields({ - [propertyName]: { - type: getTypeNameFromRef({ - typeRef: propertyTypeRef, - isInput: true, - isRequired, - }), - extensions: { propertyObj }, - }, - }); - const field: ObjectTypeComposerFieldConfigDefinition = { - type: getTypeNameFromRef({ - typeRef: propertyTypeRef, - isInput: false, - isRequired, - }), - extensions: { propertyObj }, - }; - abstractType?.addFields({ - [propertyName]: field, - }); - outputType.addFields({ - [propertyName]: field, - }); - }); - typeObj.NavigationProperty?.forEach((navigationPropertyObj: any) => { - const navigationPropertyName = navigationPropertyObj.attributes.Name; - extensions.entityInfo.navigationFields.push(navigationPropertyName); - const navigationPropertyTypeRef = navigationPropertyObj.attributes.Type; - const isRequired = navigationPropertyObj.attributes.Nullable === 'false'; - const isList = navigationPropertyTypeRef.startsWith('Collection('); - if (isList) { - const singularField: ObjectTypeComposerFieldConfigDefinition = { - type: getTypeNameFromRef({ - typeRef: navigationPropertyTypeRef, - isInput: false, - isRequired, - }) - .replace('[', '') - .replace(']', ''), - args: { - ...commonArgs, - id: { - type: 'ID', - }, - }, - extensions: { navigationPropertyObj }, - resolve: async (root, args, context, info) => { - if (navigationPropertyName in root) { - return root[navigationPropertyName]; - } - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + navigationPropertyName); - const returnType = info.returnType as GraphQLObjectType; - const { entityInfo } = returnType.extensions as EntityTypeExtensions; - addIdentifierToUrl(url, entityInfo.identifierFieldName, entityInfo.identifierFieldTypeRef, args); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - const pluralField: ObjectTypeComposerFieldConfigDefinition = { - type: getTypeNameFromRef({ - typeRef: navigationPropertyTypeRef, - isInput: false, - isRequired, - }), - args: { - ...commonArgs, - queryOptions: { type: 'QueryOptions' }, - }, - extensions: { navigationPropertyObj }, - resolve: async (root, args, context, info) => { - if (navigationPropertyName in root) { - return root[navigationPropertyName]; - } - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + navigationPropertyName); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - abstractType?.addFields({ - [navigationPropertyName]: pluralField, - [`${navigationPropertyName}ById`]: singularField, - }); - outputType.addFields({ - [navigationPropertyName]: pluralField, - [`${navigationPropertyName}ById`]: singularField, - }); - } else { - const field: ObjectTypeComposerFieldConfigDefinition = { - type: getTypeNameFromRef({ - typeRef: navigationPropertyTypeRef, - isInput: false, - isRequired, - }), - args: { - ...commonArgs, - }, - extensions: { navigationPropertyObj }, - resolve: async (root, args, context, info) => { - if (navigationPropertyName in root) { - return root[navigationPropertyName]; - } - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + navigationPropertyName); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - abstractType?.addFields({ - [navigationPropertyName]: field, - }); - outputType.addFields({ - [navigationPropertyName]: field, - }); - } - }); - if (isOpenType || outputType.getFieldNames().length === 0) { - extensions.entityInfo.isOpenType = true; - inputType.addFields({ - rest: { - type: 'JSON', - }, - }); - abstractType?.addFields({ - rest: { - type: 'JSON', - resolve: (root: any) => root, - }, - }); - outputType.addFields({ - rest: { - type: 'JSON', - resolve: (root: any) => root, - }, - }); - } - const updateInputType = inputType.clone(`${entityTypeName}UpdateInput`); - updateInputType.getFieldNames()?.forEach(fieldName => updateInputType.makeOptional(fieldName)); - // Types might be considered as unused implementations of interfaces so we must prevent that - schemaComposer.addSchemaMustHaveType(outputType); - }); - - const handleUnboundFunctionObj = (unboundFunctionObj: any) => { - const functionName = unboundFunctionObj.attributes.Name; - const returnTypeRef = unboundFunctionObj.ReturnType[0].attributes.Type; - const returnType = getTypeNameFromRef({ - typeRef: returnTypeRef, - isInput: false, - isRequired: false, - }); - schemaComposer.Query.addFields({ - [functionName]: { - type: returnType, - args: { - ...commonArgs, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + functionName); - url.href += `(${Object.entries(args) - .filter(argEntry => argEntry[0] !== 'queryOptions') - .map(argEntry => argEntry.join(' = ')) - .join(', ')})`; - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }); - unboundFunctionObj.Parameter?.forEach((parameterObj: any) => { - const parameterName = parameterObj.attributes.Name; - const parameterTypeRef = parameterObj.attributes.Type; - const isRequired = parameterObj.attributes.Nullable === 'false'; - const parameterType = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: true, - isRequired, - }); - schemaComposer.Query.addFieldArgs(functionName, { - [parameterName]: { - type: parameterType, - }, - }); - }); - }; - - const handleBoundFunctionObj = (boundFunctionObj: any) => { - const functionName = boundFunctionObj.attributes.Name; - const functionRef = schemaNamespace + '.' + functionName; - const returnTypeRef = boundFunctionObj.ReturnType[0].attributes.Type; - const returnType = getTypeNameFromRef({ - typeRef: returnTypeRef, - isInput: false, - isRequired: false, - }); - const args: ObjectTypeComposerArgumentConfigMapDefinition = { - ...commonArgs, - }; - // eslint-disable-next-line prefer-const - let entitySetPath = boundFunctionObj.attributes.EntitySetPath; - boundFunctionObj.Parameter?.forEach((parameterObj: any) => { - const parameterName = parameterObj.attributes.Name; - const parameterTypeRef = parameterObj.attributes.Type; - const isRequired = parameterObj.attributes.Nullable === 'false'; - const parameterTypeName = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: true, - isRequired, - }); - // If entitySetPath is not available, take first parameter as entity - entitySetPath = entitySetPath || parameterName; - if (entitySetPath === parameterName) { - const boundEntityTypeName = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: false, - isRequired: false, - }) - .replace('[', '') - .replace(']', ''); - const boundEntityType = schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer; - const boundEntityOtherType = getTCByTypeNames( - 'I' + boundEntityTypeName, - 'T' + boundEntityTypeName - ) as InterfaceTypeComposer; - const field: ObjectTypeComposerFieldConfigDefinition = { - type: returnType, - args, - resolve: async (root, args, context, info) => { - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + functionRef); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - boundEntityType.addFields({ - [functionName]: field, - }); - boundEntityOtherType?.addFields({ - [functionName]: field, - }); - } - args[parameterName] = { - type: parameterTypeName, - }; - }); - }; - - schemaObj.Function?.forEach((functionObj: any) => { - if (functionObj.attributes?.IsBound === 'true') { - handleBoundFunctionObj(functionObj); - } else { - handleUnboundFunctionObj(functionObj); - } - }); - - const handleUnboundActionObj = (unboundActionObj: any) => { - const actionName = unboundActionObj.attributes.Name; - schemaComposer.Mutation.addFields({ - [actionName]: { - type: 'JSON', - args: { - ...commonArgs, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + actionName); - const urlString = getUrlString(url); - const method = 'POST'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - body: jsonFlatStringify(args), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }); - - unboundActionObj.Parameter?.forEach((parameterObj: any) => { - const parameterName = parameterObj.attributes.Name; - const parameterTypeRef = parameterObj.attributes.Type; - const isRequired = parameterObj.attributes.Nullable === 'false'; - const parameterType = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: true, - isRequired, - }); - schemaComposer.Mutation.addFieldArgs(actionName, { - [parameterName]: { - type: parameterType, - }, - }); - }); - }; - - const handleBoundActionObj = (boundActionObj: any) => { - const actionName = boundActionObj.attributes.Name; - const actionRef = schemaNamespace + '.' + actionName; - const args: ObjectTypeComposerArgumentConfigMapDefinition = { - ...commonArgs, - }; - let entitySetPath = boundActionObj.attributes.EntitySetPath; - let boundField: ObjectTypeComposerFieldConfigDefinition; - let boundEntityTypeName: string; - boundActionObj.Parameter?.forEach((parameterObj: any) => { - const parameterName = parameterObj.attributes.Name; - const parameterTypeRef = parameterObj.attributes.Type; - const isRequired = parameterObj.attributes.Nullable === 'false'; - const parameterTypeName = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: true, - isRequired, - }); - // If entitySetPath is not available, take first parameter as entity - entitySetPath = entitySetPath || parameterName; - if (entitySetPath === parameterName) { - boundEntityTypeName = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: false, - isRequired: false, - }) - .replace('[', '') - .replace(']', ''); // Todo temp workaround - boundField = { - type: 'JSON', - args, - resolve: async (root, args, context, info) => { - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + actionRef); - const urlString = getUrlString(url); - const method = 'POST'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - body: jsonFlatStringify(args), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - } - args[parameterName] = { - type: parameterTypeName, - }; - }); - const boundEntityType = schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer; - boundEntityType.addFields({ - [actionName]: boundField, - }); - const otherType = getTCByTypeNames( - `I${boundEntityTypeName}`, - `T${boundEntityTypeName}` - ) as InterfaceTypeComposer; - otherType?.addFields({ - [actionName]: boundField, - }); - }; - - schemaObj.Action?.forEach((actionObj: any) => { - if (actionObj.attributes?.IsBound === 'true') { - handleBoundActionObj(actionObj); - } else { - handleUnboundActionObj(actionObj); - } - }); - - // Rearrange fields for base types and implementations - typesWithBaseType?.forEach((typeObj: any) => { - const typeName = buildName({ - schemaNamespace, - name: typeObj.attributes.Name, - }); - const inputType = schemaComposer.getITC(typeName + 'Input') as InputTypeComposer; - const abstractType = getTCByTypeNames('I' + typeName, typeName) as InterfaceTypeComposer; - const outputType = getTCByTypeNames('T' + typeName, typeName) as ObjectTypeComposer; - const baseTypeRef = typeObj.attributes.BaseType; - const { entityInfo, eventEmitter } = outputType.getExtensions() as EntityTypeExtensions; - const baseTypeName = getTypeNameFromRef({ - typeRef: baseTypeRef, - isInput: false, - isRequired: false, - }); - const baseInputType = schemaComposer.getAnyTC(baseTypeName + 'Input') as InputTypeComposer; - const baseAbstractType = getTCByTypeNames('I' + baseTypeName, baseTypeName) as InterfaceTypeComposer; - const baseOutputType = getTCByTypeNames('T' + baseTypeName, baseTypeName) as ObjectTypeComposer; - const { entityInfo: baseEntityInfo, eventEmitter: baseEventEmitter } = - baseOutputType.getExtensions() as EntityTypeExtensions; - const baseEventEmitterListener = () => { - inputType.addFields(baseInputType.getFields()); - entityInfo.identifierFieldName = baseEntityInfo.identifierFieldName || entityInfo.identifierFieldName; - entityInfo.identifierFieldTypeRef = - baseEntityInfo.identifierFieldTypeRef || entityInfo.identifierFieldTypeRef; - entityInfo.actualFields.unshift(...baseEntityInfo.actualFields); - abstractType?.addFields(baseAbstractType?.getFields()); - outputType.addFields(baseOutputType.getFields()); - if (baseAbstractType instanceof InterfaceTypeComposer) { - // abstractType.addInterface(baseAbstractType.getTypeName()); - outputType.addInterface(baseAbstractType.getTypeName()); - } - eventEmitter.emit('onFieldChange'); - }; - baseEventEmitter.on('onFieldChange', baseEventEmitterListener); - baseEventEmitterListener(); - }); - - schemaObj.EntityContainer?.forEach((entityContainerObj: any) => { - entityContainerObj.Singleton?.forEach((singletonObj: any) => { - const singletonName = singletonObj.attributes.Name; - const singletonTypeRef = singletonObj.attributes.Type; - const singletonTypeName = getTypeNameFromRef({ - typeRef: singletonTypeRef, - isInput: false, - isRequired: false, - }); - schemaComposer.Query.addFields({ - [singletonName]: { - type: singletonTypeName, - args: { - ...commonArgs, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + singletonName); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }); - }); - - entityContainerObj?.EntitySet?.forEach((entitySetObj: any) => { - const entitySetName = entitySetObj.attributes.Name; - const entitySetTypeRef = entitySetObj.attributes.EntityType; - const entityTypeName = getTypeNameFromRef({ - typeRef: entitySetTypeRef, - isInput: false, - isRequired: false, - }); - const entityOutputTC = getTCByTypeNames('I' + entityTypeName, entityTypeName) as - | InterfaceTypeComposer - | ObjectTypeComposer; - const { entityInfo } = entityOutputTC.getExtensions() as EntityTypeExtensions; - const identifierFieldName = entityInfo.identifierFieldName; - const identifierFieldTypeRef = entityInfo.identifierFieldTypeRef; - const identifierFieldTypeName = entityOutputTC.getFieldTypeName(identifierFieldName); - const typeName = entityOutputTC.getTypeName(); - const commonFields: Record> = { - [entitySetName]: { - type: `[${typeName}]`, - args: { - ...commonArgs, - queryOptions: { type: 'QueryOptions' }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - [`${entitySetName}By${identifierFieldName}`]: { - type: typeName, - args: { - ...commonArgs, - [identifierFieldName]: { - type: identifierFieldTypeName, - }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = this.prepareSearchParams(parsedInfoFragment, info.schema); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }; - schemaComposer.Query.addFields({ - ...commonFields, - [`${entitySetName}Count`]: { - type: 'Int', - args: { - ...commonArgs, - queryOptions: { type: 'QueryOptions' }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, `/${entitySetName}/$count`); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return responseText; - }, - }, - }); - schemaComposer.Mutation.addFields({ - ...commonFields, - [`create${entitySetName}`]: { - type: typeName, - args: { - ...commonArgs, - input: { - type: entityTypeName + 'Input', - }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - const urlString = getUrlString(url); - rebuildOpenInputObjects(args.input); - const method = 'POST'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - body: jsonFlatStringify(args.input), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - [`delete${entitySetName}By${identifierFieldName}`]: { - type: 'JSON', - args: { - ...commonArgs, - [identifierFieldName]: { - type: identifierFieldTypeName, - }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); - const urlString = getUrlString(url); - const method = 'DELETE'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - [`update${entitySetName}By${identifierFieldName}`]: { - type: typeName, - args: { - ...commonArgs, - [identifierFieldName]: { - type: identifierFieldTypeName, - }, - input: { - type: entityTypeName + 'UpdateInput', - }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); - const urlString = getUrlString(url); - rebuildOpenInputObjects(args.input); - const method = 'PATCH'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - body: jsonFlatStringify(args.input), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }); - }); - }); + const schema = generateGraphQLSchema({ + metadataJson, + commonArgs, + contextVariables, + contextDataloaderName, + headersFactory, + config: this.config, + env, + baseUrl, + eventEmitterSet: this.eventEmitterSet }); - // graphql-compose doesn't add @defer and @stream to the schema - specifiedDirectives.forEach(directive => schemaComposer.addDirective(directive)); - - const schema = schemaComposer.buildSchema(); this.eventEmitterSet.forEach(ee => ee.removeAllListeners()); this.eventEmitterSet.clear(); @@ -1053,50 +132,4 @@ export default class ODataHandler implements MeshHandler { }), }; } - - private prepareSearchParams(fragment: ResolveTree, schema: GraphQLSchema) { - const fragmentTypeNames = Object.keys(fragment.fieldsByTypeName) as string[]; - const returnType = schema.getType(fragmentTypeNames[0]); - const { args, fields } = simplifyParsedResolveInfoFragmentWithType(fragment, returnType); - const searchParams = new URLSearchParams(); - if ('queryOptions' in args) { - const { queryOptions } = args as any; - for (const param in queryOptionsFields) { - if (param in queryOptions) { - searchParams.set('$' + param, queryOptions[param]); - } - } - } - - // $select doesn't work with inherited types' fields. So if there is an inline fragment for - // implemented types, we cannot use $select - const isSelectable = !isAbstractType(returnType); - - if (isSelectable) { - const { entityInfo } = returnType.extensions as EntityTypeExtensions; - const selectionFields: string[] = []; - const expandedFields: string[] = []; - for (const fieldName in fields) { - if (entityInfo.actualFields.includes(fieldName)) { - selectionFields.push(fieldName); - } - if (this.config.expandNavProps && entityInfo.navigationFields.includes(fieldName)) { - const searchParams = this.prepareSearchParams(fields[fieldName], schema); - const searchParamsStr = decodeURIComponent(searchParams.toString()); - expandedFields.push(`${fieldName}(${searchParamsStr.split('&').join(';')})`); - selectionFields.push(fieldName); - } - } - if (!selectionFields.includes(entityInfo.identifierFieldName)) { - selectionFields.push(entityInfo.identifierFieldName); - } - if (selectionFields.length) { - searchParams.set('$select', selectionFields.join(',')); - } - if (expandedFields.length) { - searchParams.set('$expand', expandedFields.join(',')); - } - } - return searchParams; - } } diff --git a/packages/handlers/odata/src/request-processing.ts b/packages/handlers/odata/src/request-processing.ts index 3b9e56608d725..f4a692cb71225 100644 --- a/packages/handlers/odata/src/request-processing.ts +++ b/packages/handlers/odata/src/request-processing.ts @@ -9,7 +9,7 @@ import { getUrlString, addIdentifierToUrl } from "./util"; import urljoin from 'url-join'; import { ResolverData } from "@graphql-mesh/types"; -type HeadersFactory = (resolverData: ResolverData, method: string) => Headers; +export type HeadersFactory = (resolverData: ResolverData, method: string) => Headers; type DataLoaderFactory = (context: any) => DataLoader; type DataLoaderType = 'multipart' | 'json' | 'none'; diff --git a/packages/handlers/odata/src/schema-generation.ts b/packages/handlers/odata/src/schema-generation.ts new file mode 100644 index 0000000000000..0da414ce4ef7f --- /dev/null +++ b/packages/handlers/odata/src/schema-generation.ts @@ -0,0 +1,1016 @@ +import { EventEmitter } from 'events'; +import { + isAbstractType, + GraphQLObjectType, + GraphQLSchema, + specifiedDirectives, +} from 'graphql'; +import { + SchemaComposer, + ObjectTypeComposer, + InterfaceTypeComposer, + ObjectTypeComposerFieldConfigDefinition, + ObjectTypeComposerArgumentConfigMapDefinition, + EnumTypeComposerValueConfigDefinition, + InputTypeComposer, +} from 'graphql-compose'; +import { + GraphQLBigInt, + GraphQLGUID, + GraphQLDateTime, + GraphQLJSON, + GraphQLDate, + GraphQLByte, + GraphQLISO8601Duration, +} from 'graphql-scalars'; +import { jsonFlatStringify } from "@graphql-mesh/utils"; +import { YamlConfig } from '@graphql-mesh/types'; +import { parseResolveInfo, ResolveTree, simplifyParsedResolveInfoFragmentWithType } from 'graphql-parse-resolve-info'; +import urljoin from 'url-join'; +import { pascalCase } from 'pascal-case'; +import { Request } from 'cross-fetch'; +import { SCALARS } from './scalars'; +import { queryOptionsFields } from './query-options'; +import { EntityTypeExtensions } from './schema-util'; +import { addIdentifierToUrl, getUrlString } from './util'; +import { handleResponseText, HeadersFactory } from './request-processing' + +type GenerateSchemaArgs = { + metadataJson: any, + commonArgs: Record, + contextVariables: string[], + contextDataloaderName: string | symbol, + headersFactory: HeadersFactory, + config: YamlConfig.ODataHandler, + env: Record, + baseUrl: string, + eventEmitterSet: Set +} + +export function generateGraphQLSchema({ + metadataJson, + commonArgs, + contextVariables, + contextDataloaderName, + headersFactory, + config, + env, + baseUrl, + eventEmitterSet +}: GenerateSchemaArgs): GraphQLSchema { + const schemaComposer = initSchemaComposer(); + + const aliasNamespaceMap = new Map(); + + const schemas = metadataJson.Edmx[0].DataServices[0].Schema; + const multipleSchemas = schemas.length > 1; + const namespaces = new Set(); + + function getNamespaceFromTypeRef(typeRef: string) { + let namespace = ''; + namespaces?.forEach(el => { + if ( + typeRef.startsWith(el) && + el.length > namespace.length && // It can be deeper namespace + !typeRef.replace(el + '.', '').includes('.') // Typename cannot have `.` + ) { + namespace = el; + } + }); + return namespace; + } + + function getTypeNameFromRef({ + typeRef, + isInput, + isRequired, + }: { + typeRef: string; + isInput: boolean; + isRequired: boolean; + }) { + const typeRefArr = typeRef.split('Collection('); + const arrayDepth = typeRefArr.length; + let actualTypeRef = typeRefArr.join('').split(')').join(''); + const typeNamespace = getNamespaceFromTypeRef(actualTypeRef); + if (aliasNamespaceMap.has(typeNamespace)) { + const alias = aliasNamespaceMap.get(typeNamespace); + actualTypeRef = actualTypeRef.replace(typeNamespace, alias); + } + const actualTypeRefArr = actualTypeRef.split('.'); + const typeName = multipleSchemas + ? pascalCase(actualTypeRefArr.join('_')) + : actualTypeRefArr[actualTypeRefArr.length - 1]; + let realTypeName = typeName; + if (SCALARS.has(actualTypeRef)) { + realTypeName = SCALARS.get(actualTypeRef); + } else if (schemaComposer.isEnumType(typeName)) { + realTypeName = typeName; + } else if (isInput) { + realTypeName += 'Input'; + } + const fakeEmptyArr = new Array(arrayDepth); + realTypeName = fakeEmptyArr.join('[') + realTypeName + fakeEmptyArr.join(']'); + if (isRequired) { + realTypeName += '!'; + } + return realTypeName; + } + + schemaComposer.createEnumTC({ + name: 'InlineCount', + values: { + allpages: { + value: 'allpages', + description: + 'The OData MUST include a count of the number of entities in the collection identified by the URI (after applying any $filter System Query Options present on the URI)', + }, + none: { + value: 'none', + description: + 'The OData service MUST NOT include a count in the response. This is equivalence to a URI that does not include a $inlinecount query string parameter.', + }, + }, + }); + + schemaComposer.createInputTC({ + name: 'QueryOptions', + fields: queryOptionsFields, + }); + + function getTCByTypeNames(...typeNames: string[]) { + for (const typeName of typeNames) { + try { + return schemaComposer.getAnyTC(typeName); + } catch {} + } + return null; + } + + function rebuildOpenInputObjects(input: any) { + if (typeof input === 'object') { + if ('rest' in input) { + Object.assign(input, input.rest); + delete input.rest; + } + for (const fieldName in input) { + rebuildOpenInputObjects(input[fieldName]); + } + } + } + + function buildName({ schemaNamespace, name }: { schemaNamespace: string; name: string }) { + const alias = aliasNamespaceMap.get(schemaNamespace) || schemaNamespace; + const ref = alias + '.' + name; + return multipleSchemas ? pascalCase(ref.split('.').join('_')) : name; + } + + schemas?.forEach((schemaObj: any) => { + const schemaNamespace = schemaObj.attributes.Namespace; + namespaces.add(schemaNamespace); + const schemaAlias = schemaObj.attributes.Alias; + if (schemaAlias) { + aliasNamespaceMap.set(schemaNamespace, schemaAlias); + } + }); + + // start schemas.forEach loop + schemas?.forEach((schemaObj: any) => { + const schemaNamespace = schemaObj.attributes.Namespace; + + schemaObj.EnumType?.forEach((enumObj: any) => { + const values: Record = {}; + enumObj.Member?.forEach((memberObj: any) => { + const key = memberObj.attributes.Name; + // This doesn't work. + // const value = memberElement.getAttribute('Value')!; + values[key] = { + value: key, + extensions: { memberObj }, + }; + }); + const enumTypeName = buildName({ schemaNamespace, name: enumObj.attributes.Name }); + schemaComposer.createEnumTC({ + name: enumTypeName, + values, + extensions: { enumObj }, + }); + }); + + const allTypes = (schemaObj.EntityType || []).concat(schemaObj.ComplexType || []); + const typesWithBaseType = allTypes.filter((typeObj: any) => typeObj.attributes.BaseType); + + allTypes?.forEach((typeObj: any) => { + const entityTypeName = buildName({ schemaNamespace, name: typeObj.attributes.Name }); + const isOpenType = typeObj.attributes.OpenType === 'true'; + const isAbstract = typeObj.attributes.Abstract === 'true'; + const eventEmitter = new EventEmitter(); + eventEmitter.setMaxListeners(Infinity); + eventEmitterSet.add(eventEmitter); + const extensions: EntityTypeExtensions = { + entityInfo: { + actualFields: [], + navigationFields: [], + isOpenType, + }, + typeObj, + eventEmitter, + }; + const inputType = schemaComposer.createInputTC({ + name: entityTypeName + 'Input', + fields: {}, + extensions: () => extensions, + }); + let abstractType: InterfaceTypeComposer; + if ( + typesWithBaseType.some((typeObj: any) => typeObj.attributes.BaseType.includes(`.${entityTypeName}`)) || + isAbstract + ) { + abstractType = schemaComposer.createInterfaceTC({ + name: isAbstract ? entityTypeName : `I${entityTypeName}`, + extensions, + resolveType: (root: any) => { + const typeRef = root['@odata.type']?.replace('#', ''); + if (typeRef) { + const typeName = getTypeNameFromRef({ + typeRef: root['@odata.type'].replace('#', ''), + isInput: false, + isRequired: false, + }); + return typeName; + } + return isAbstract ? `T${entityTypeName}` : entityTypeName; + }, + }); + } + const outputType = schemaComposer.createObjectTC({ + name: isAbstract ? `T${entityTypeName}` : entityTypeName, + extensions, + interfaces: abstractType ? [abstractType] : [], + }); + + abstractType?.setInputTypeComposer(inputType); + outputType.setInputTypeComposer(inputType); + + const propertyRefObj = typeObj.Key && typeObj.Key[0].PropertyRef[0]; + if (propertyRefObj) { + extensions.entityInfo.identifierFieldName = propertyRefObj.attributes.Name; + } + + typeObj.Property?.forEach((propertyObj: any) => { + const propertyName = propertyObj.attributes.Name; + extensions.entityInfo.actualFields.push(propertyName); + const propertyTypeRef = propertyObj.attributes.Type; + if (propertyName === extensions.entityInfo.identifierFieldName) { + extensions.entityInfo.identifierFieldTypeRef = propertyTypeRef; + } + const isRequired = propertyObj.attributes.Nullable === 'false'; + inputType.addFields({ + [propertyName]: { + type: getTypeNameFromRef({ + typeRef: propertyTypeRef, + isInput: true, + isRequired, + }), + extensions: { propertyObj }, + }, + }); + const field: ObjectTypeComposerFieldConfigDefinition = { + type: getTypeNameFromRef({ + typeRef: propertyTypeRef, + isInput: false, + isRequired, + }), + extensions: { propertyObj }, + }; + abstractType?.addFields({ + [propertyName]: field, + }); + outputType.addFields({ + [propertyName]: field, + }); + }); + typeObj.NavigationProperty?.forEach((navigationPropertyObj: any) => { + const navigationPropertyName = navigationPropertyObj.attributes.Name; + extensions.entityInfo.navigationFields.push(navigationPropertyName); + const navigationPropertyTypeRef = navigationPropertyObj.attributes.Type; + const isRequired = navigationPropertyObj.attributes.Nullable === 'false'; + const isList = navigationPropertyTypeRef.startsWith('Collection('); + if (isList) { + const singularField: ObjectTypeComposerFieldConfigDefinition = { + type: getTypeNameFromRef({ + typeRef: navigationPropertyTypeRef, + isInput: false, + isRequired, + }) + .replace('[', '') + .replace(']', ''), + args: { + ...commonArgs, + id: { + type: 'ID', + }, + }, + extensions: { navigationPropertyObj }, + resolve: async (root, args, context, info) => { + if (navigationPropertyName in root) { + return root[navigationPropertyName]; + } + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + navigationPropertyName); + const returnType = info.returnType as GraphQLObjectType; + const { entityInfo } = returnType.extensions as EntityTypeExtensions; + addIdentifierToUrl(url, entityInfo.identifierFieldName, entityInfo.identifierFieldTypeRef, args); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + const pluralField: ObjectTypeComposerFieldConfigDefinition = { + type: getTypeNameFromRef({ + typeRef: navigationPropertyTypeRef, + isInput: false, + isRequired, + }), + args: { + ...commonArgs, + queryOptions: { type: 'QueryOptions' }, + }, + extensions: { navigationPropertyObj }, + resolve: async (root, args, context, info) => { + if (navigationPropertyName in root) { + return root[navigationPropertyName]; + } + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + navigationPropertyName); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + abstractType?.addFields({ + [navigationPropertyName]: pluralField, + [`${navigationPropertyName}ById`]: singularField, + }); + outputType.addFields({ + [navigationPropertyName]: pluralField, + [`${navigationPropertyName}ById`]: singularField, + }); + } else { + const field: ObjectTypeComposerFieldConfigDefinition = { + type: getTypeNameFromRef({ + typeRef: navigationPropertyTypeRef, + isInput: false, + isRequired, + }), + args: { + ...commonArgs, + }, + extensions: { navigationPropertyObj }, + resolve: async (root, args, context, info) => { + if (navigationPropertyName in root) { + return root[navigationPropertyName]; + } + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + navigationPropertyName); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + abstractType?.addFields({ + [navigationPropertyName]: field, + }); + outputType.addFields({ + [navigationPropertyName]: field, + }); + } + }); + if (isOpenType || outputType.getFieldNames().length === 0) { + extensions.entityInfo.isOpenType = true; + inputType.addFields({ + rest: { + type: 'JSON', + }, + }); + abstractType?.addFields({ + rest: { + type: 'JSON', + resolve: (root: any) => root, + }, + }); + outputType.addFields({ + rest: { + type: 'JSON', + resolve: (root: any) => root, + }, + }); + } + const updateInputType = inputType.clone(`${entityTypeName}UpdateInput`); + updateInputType.getFieldNames()?.forEach(fieldName => updateInputType.makeOptional(fieldName)); + // Types might be considered as unused implementations of interfaces so we must prevent that + schemaComposer.addSchemaMustHaveType(outputType); + }); + + const handleUnboundFunctionObj = (unboundFunctionObj: any) => { + const functionName = unboundFunctionObj.attributes.Name; + const returnTypeRef = unboundFunctionObj.ReturnType[0].attributes.Type; + const returnType = getTypeNameFromRef({ + typeRef: returnTypeRef, + isInput: false, + isRequired: false, + }); + schemaComposer.Query.addFields({ + [functionName]: { + type: returnType, + args: { + ...commonArgs, + }, + resolve: async (root, args, context, info) => { + const url = new URL(baseUrl); + url.href = urljoin(url.href, '/' + functionName); + url.href += `(${Object.entries(args) + .filter(argEntry => argEntry[0] !== 'queryOptions') + .map(argEntry => argEntry.join(' = ')) + .join(', ')})`; + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }); + unboundFunctionObj.Parameter?.forEach((parameterObj: any) => { + const parameterName = parameterObj.attributes.Name; + const parameterTypeRef = parameterObj.attributes.Type; + const isRequired = parameterObj.attributes.Nullable === 'false'; + const parameterType = getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: true, + isRequired, + }); + schemaComposer.Query.addFieldArgs(functionName, { + [parameterName]: { + type: parameterType, + }, + }); + }); + }; + + const handleBoundFunctionObj = (boundFunctionObj: any) => { + const functionName = boundFunctionObj.attributes.Name; + const functionRef = schemaNamespace + '.' + functionName; + const returnTypeRef = boundFunctionObj.ReturnType[0].attributes.Type; + const returnType = getTypeNameFromRef({ + typeRef: returnTypeRef, + isInput: false, + isRequired: false, + }); + const args: ObjectTypeComposerArgumentConfigMapDefinition = { + ...commonArgs, + }; + // eslint-disable-next-line prefer-const + let entitySetPath = boundFunctionObj.attributes.EntitySetPath; + boundFunctionObj.Parameter?.forEach((parameterObj: any) => { + const parameterName = parameterObj.attributes.Name; + const parameterTypeRef = parameterObj.attributes.Type; + const isRequired = parameterObj.attributes.Nullable === 'false'; + const parameterTypeName = getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: true, + isRequired, + }); + // If entitySetPath is not available, take first parameter as entity + entitySetPath = entitySetPath || parameterName; + if (entitySetPath === parameterName) { + const boundEntityTypeName = getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: false, + isRequired: false, + }) + .replace('[', '') + .replace(']', ''); + const boundEntityType = schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer; + const boundEntityOtherType = getTCByTypeNames( + 'I' + boundEntityTypeName, + 'T' + boundEntityTypeName + ) as InterfaceTypeComposer; + const field: ObjectTypeComposerFieldConfigDefinition = { + type: returnType, + args, + resolve: async (root, args, context, info) => { + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + functionRef); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + boundEntityType.addFields({ + [functionName]: field, + }); + boundEntityOtherType?.addFields({ + [functionName]: field, + }); + } + args[parameterName] = { + type: parameterTypeName, + }; + }); + }; + + schemaObj.Function?.forEach((functionObj: any) => { + if (functionObj.attributes?.IsBound === 'true') { + handleBoundFunctionObj(functionObj); + } else { + handleUnboundFunctionObj(functionObj); + } + }); + + const handleUnboundActionObj = (unboundActionObj: any) => { + const actionName = unboundActionObj.attributes.Name; + schemaComposer.Mutation.addFields({ + [actionName]: { + type: 'JSON', + args: { + ...commonArgs, + }, + resolve: async (root, args, context, info) => { + const url = new URL(baseUrl); + url.href = urljoin(url.href, '/' + actionName); + const urlString = getUrlString(url); + const method = 'POST'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + body: jsonFlatStringify(args), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }); + + unboundActionObj.Parameter?.forEach((parameterObj: any) => { + const parameterName = parameterObj.attributes.Name; + const parameterTypeRef = parameterObj.attributes.Type; + const isRequired = parameterObj.attributes.Nullable === 'false'; + const parameterType = getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: true, + isRequired, + }); + schemaComposer.Mutation.addFieldArgs(actionName, { + [parameterName]: { + type: parameterType, + }, + }); + }); + }; + + const handleBoundActionObj = (boundActionObj: any) => { + const actionName = boundActionObj.attributes.Name; + const actionRef = schemaNamespace + '.' + actionName; + const args: ObjectTypeComposerArgumentConfigMapDefinition = { + ...commonArgs, + }; + let entitySetPath = boundActionObj.attributes.EntitySetPath; + let boundField: ObjectTypeComposerFieldConfigDefinition; + let boundEntityTypeName: string; + boundActionObj.Parameter?.forEach((parameterObj: any) => { + const parameterName = parameterObj.attributes.Name; + const parameterTypeRef = parameterObj.attributes.Type; + const isRequired = parameterObj.attributes.Nullable === 'false'; + const parameterTypeName = getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: true, + isRequired, + }); + // If entitySetPath is not available, take first parameter as entity + entitySetPath = entitySetPath || parameterName; + if (entitySetPath === parameterName) { + boundEntityTypeName = getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: false, + isRequired: false, + }) + .replace('[', '') + .replace(']', ''); // Todo temp workaround + boundField = { + type: 'JSON', + args, + resolve: async (root, args, context, info) => { + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + actionRef); + const urlString = getUrlString(url); + const method = 'POST'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + body: jsonFlatStringify(args), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + } + args[parameterName] = { + type: parameterTypeName, + }; + }); + const boundEntityType = schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer; + boundEntityType.addFields({ + [actionName]: boundField, + }); + const otherType = getTCByTypeNames( + `I${boundEntityTypeName}`, + `T${boundEntityTypeName}` + ) as InterfaceTypeComposer; + otherType?.addFields({ + [actionName]: boundField, + }); + }; + + schemaObj.Action?.forEach((actionObj: any) => { + if (actionObj.attributes?.IsBound === 'true') { + handleBoundActionObj(actionObj); + } else { + handleUnboundActionObj(actionObj); + } + }); + + // Rearrange fields for base types and implementations + typesWithBaseType?.forEach((typeObj: any) => { + const typeName = buildName({ + schemaNamespace, + name: typeObj.attributes.Name, + }); + const inputType = schemaComposer.getITC(typeName + 'Input') as InputTypeComposer; + const abstractType = getTCByTypeNames('I' + typeName, typeName) as InterfaceTypeComposer; + const outputType = getTCByTypeNames('T' + typeName, typeName) as ObjectTypeComposer; + const baseTypeRef = typeObj.attributes.BaseType; + const { entityInfo, eventEmitter } = outputType.getExtensions() as EntityTypeExtensions; + const baseTypeName = getTypeNameFromRef({ + typeRef: baseTypeRef, + isInput: false, + isRequired: false, + }); + const baseInputType = schemaComposer.getAnyTC(baseTypeName + 'Input') as InputTypeComposer; + const baseAbstractType = getTCByTypeNames('I' + baseTypeName, baseTypeName) as InterfaceTypeComposer; + const baseOutputType = getTCByTypeNames('T' + baseTypeName, baseTypeName) as ObjectTypeComposer; + const { entityInfo: baseEntityInfo, eventEmitter: baseEventEmitter } = + baseOutputType.getExtensions() as EntityTypeExtensions; + const baseEventEmitterListener = () => { + inputType.addFields(baseInputType.getFields()); + entityInfo.identifierFieldName = baseEntityInfo.identifierFieldName || entityInfo.identifierFieldName; + entityInfo.identifierFieldTypeRef = + baseEntityInfo.identifierFieldTypeRef || entityInfo.identifierFieldTypeRef; + entityInfo.actualFields.unshift(...baseEntityInfo.actualFields); + abstractType?.addFields(baseAbstractType?.getFields()); + outputType.addFields(baseOutputType.getFields()); + if (baseAbstractType instanceof InterfaceTypeComposer) { + // abstractType.addInterface(baseAbstractType.getTypeName()); + outputType.addInterface(baseAbstractType.getTypeName()); + } + eventEmitter.emit('onFieldChange'); + }; + baseEventEmitter.on('onFieldChange', baseEventEmitterListener); + baseEventEmitterListener(); + }); + + schemaObj.EntityContainer?.forEach((entityContainerObj: any) => { + entityContainerObj.Singleton?.forEach((singletonObj: any) => { + const singletonName = singletonObj.attributes.Name; + const singletonTypeRef = singletonObj.attributes.Type; + const singletonTypeName = getTypeNameFromRef({ + typeRef: singletonTypeRef, + isInput: false, + isRequired: false, + }); + schemaComposer.Query.addFields({ + [singletonName]: { + type: singletonTypeName, + args: { + ...commonArgs, + }, + resolve: async (root, args, context, info) => { + const url = new URL(baseUrl); + url.href = urljoin(url.href, '/' + singletonName); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }); + }); + + entityContainerObj?.EntitySet?.forEach((entitySetObj: any) => { + const entitySetName = entitySetObj.attributes.Name; + const entitySetTypeRef = entitySetObj.attributes.EntityType; + const entityTypeName = getTypeNameFromRef({ + typeRef: entitySetTypeRef, + isInput: false, + isRequired: false, + }); + const entityOutputTC = getTCByTypeNames('I' + entityTypeName, entityTypeName) as + | InterfaceTypeComposer + | ObjectTypeComposer; + const { entityInfo } = entityOutputTC.getExtensions() as EntityTypeExtensions; + const identifierFieldName = entityInfo.identifierFieldName; + const identifierFieldTypeRef = entityInfo.identifierFieldTypeRef; + const identifierFieldTypeName = entityOutputTC.getFieldTypeName(identifierFieldName); + const typeName = entityOutputTC.getTypeName(); + const commonFields: Record> = { + [entitySetName]: { + type: `[${typeName}]`, + args: { + ...commonArgs, + queryOptions: { type: 'QueryOptions' }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + [`${entitySetName}By${identifierFieldName}`]: { + type: typeName, + args: { + ...commonArgs, + [identifierFieldName]: { + type: identifierFieldTypeName, + }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }; + schemaComposer.Query.addFields({ + ...commonFields, + [`${entitySetName}Count`]: { + type: 'Int', + args: { + ...commonArgs, + queryOptions: { type: 'QueryOptions' }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(baseUrl); + url.href = urljoin(url.href, `/${entitySetName}/$count`); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return responseText; + }, + }, + }); + schemaComposer.Mutation.addFields({ + ...commonFields, + [`create${entitySetName}`]: { + type: typeName, + args: { + ...commonArgs, + input: { + type: entityTypeName + 'Input', + }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + const urlString = getUrlString(url); + rebuildOpenInputObjects(args.input); + const method = 'POST'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + body: jsonFlatStringify(args.input), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + [`delete${entitySetName}By${identifierFieldName}`]: { + type: 'JSON', + args: { + ...commonArgs, + [identifierFieldName]: { + type: identifierFieldTypeName, + }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); + const urlString = getUrlString(url); + const method = 'DELETE'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + [`update${entitySetName}By${identifierFieldName}`]: { + type: typeName, + args: { + ...commonArgs, + [identifierFieldName]: { + type: identifierFieldTypeName, + }, + input: { + type: entityTypeName + 'UpdateInput', + }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); + const urlString = getUrlString(url); + rebuildOpenInputObjects(args.input); + const method = 'PATCH'; + const request = new Request(urlString, { + method, + headers: headersFactory({ root, args, context, info, env }, method), + body: jsonFlatStringify(args.input), + }); + const response = await context[contextDataloaderName].load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }); + }); + }); + }); + // end schemas.forEach loop + + // graphql-compose doesn't add @defer and @stream to the schema + specifiedDirectives.forEach(directive => schemaComposer.addDirective(directive)); + + const schema = schemaComposer.buildSchema(); + return schema; +} + + +function initSchemaComposer() { + const schemaComposer = new SchemaComposer(); + schemaComposer.add(GraphQLBigInt); + schemaComposer.add(GraphQLGUID); + schemaComposer.add(GraphQLDateTime); + schemaComposer.add(GraphQLJSON); + schemaComposer.add(GraphQLByte); + schemaComposer.add(GraphQLDate); + schemaComposer.add(GraphQLISO8601Duration); + + return schemaComposer; +} + +function prepareSearchParams(fragment: ResolveTree, schema: GraphQLSchema, config: YamlConfig.ODataHandler) { + const fragmentTypeNames = Object.keys(fragment.fieldsByTypeName) as string[]; + const returnType = schema.getType(fragmentTypeNames[0]); + const { args, fields } = simplifyParsedResolveInfoFragmentWithType(fragment, returnType); + const searchParams = new URLSearchParams(); + if ('queryOptions' in args) { + const { queryOptions } = args as any; + for (const param in queryOptionsFields) { + if (param in queryOptions) { + searchParams.set('$' + param, queryOptions[param]); + } + } + } + + // $select doesn't work with inherited types' fields. So if there is an inline fragment for + // implemented types, we cannot use $select + const isSelectable = !isAbstractType(returnType); + + if (isSelectable) { + const { entityInfo } = returnType.extensions as EntityTypeExtensions; + const selectionFields: string[] = []; + const expandedFields: string[] = []; + for (const fieldName in fields) { + if (entityInfo.actualFields.includes(fieldName)) { + selectionFields.push(fieldName); + } + if (config.expandNavProps && entityInfo.navigationFields.includes(fieldName)) { + const searchParams = prepareSearchParams(fields[fieldName], schema, config); + const searchParamsStr = decodeURIComponent(searchParams.toString()); + expandedFields.push(`${fieldName}(${searchParamsStr.split('&').join(';')})`); + selectionFields.push(fieldName); + } + } + if (!selectionFields.includes(entityInfo.identifierFieldName)) { + selectionFields.push(entityInfo.identifierFieldName); + } + if (selectionFields.length) { + searchParams.set('$select', selectionFields.join(',')); + } + if (expandedFields.length) { + searchParams.set('$expand', expandedFields.join(',')); + } + } + return searchParams; +} From 457db9722d808204e4e0e1bf6f73119233179a30 Mon Sep 17 00:00:00 2001 From: Clement Habinshuti Date: Thu, 15 Jul 2021 18:32:48 +0300 Subject: [PATCH 4/6] Refactor schema builder into a class in OData handler --- packages/handlers/odata/src/index.ts | 4 +- packages/handlers/odata/src/schema-builder.ts | 1071 +++++++++++++++++ .../handlers/odata/src/schema-generation.ts | 1016 ---------------- 3 files changed, 1073 insertions(+), 1018 deletions(-) create mode 100644 packages/handlers/odata/src/schema-builder.ts delete mode 100644 packages/handlers/odata/src/schema-generation.ts diff --git a/packages/handlers/odata/src/index.ts b/packages/handlers/odata/src/index.ts index d7c01e99b83d6..8aea0698d834b 100644 --- a/packages/handlers/odata/src/index.ts +++ b/packages/handlers/odata/src/index.ts @@ -22,7 +22,7 @@ import { pruneSchema } from '@graphql-tools/utils'; import { PredefinedProxyOptions } from '@graphql-mesh/store'; import { env } from 'process'; import { getDataLoaderFactory } from './request-processing'; -import { generateGraphQLSchema } from "./schema-generation"; +import { buildGraphQLSchema } from "./schema-builder"; export default class ODataHandler implements MeshHandler { @@ -109,7 +109,7 @@ export default class ODataHandler implements MeshHandler { const dataLoaderFactory = getDataLoaderFactory(this.config.batch || 'none', baseUrl, env, headersFactory, fetch); - const schema = generateGraphQLSchema({ + const schema = buildGraphQLSchema({ metadataJson, commonArgs, contextVariables, diff --git a/packages/handlers/odata/src/schema-builder.ts b/packages/handlers/odata/src/schema-builder.ts new file mode 100644 index 0000000000000..cdec707a2cf99 --- /dev/null +++ b/packages/handlers/odata/src/schema-builder.ts @@ -0,0 +1,1071 @@ +import { EventEmitter } from 'events'; +import { + isAbstractType, + GraphQLObjectType, + GraphQLSchema, + specifiedDirectives, +} from 'graphql'; +import { + SchemaComposer, + ObjectTypeComposer, + InterfaceTypeComposer, + ObjectTypeComposerFieldConfigDefinition, + ObjectTypeComposerArgumentConfigMapDefinition, + EnumTypeComposerValueConfigDefinition, + InputTypeComposer, +} from 'graphql-compose'; +import { + GraphQLBigInt, + GraphQLGUID, + GraphQLDateTime, + GraphQLJSON, + GraphQLDate, + GraphQLByte, + GraphQLISO8601Duration, +} from 'graphql-scalars'; +import { jsonFlatStringify } from "@graphql-mesh/utils"; +import { YamlConfig } from '@graphql-mesh/types'; +import { parseResolveInfo, ResolveTree, simplifyParsedResolveInfoFragmentWithType } from 'graphql-parse-resolve-info'; +import urljoin from 'url-join'; +import { pascalCase } from 'pascal-case'; +import { Request } from 'cross-fetch'; +import { SCALARS } from './scalars'; +import { queryOptionsFields } from './query-options'; +import { EntityTypeExtensions } from './schema-util'; +import { addIdentifierToUrl, getUrlString } from './util'; +import { handleResponseText, HeadersFactory } from './request-processing' +import DataLoader from 'dataloader'; + +type SchemaBuilderArgs = { + metadataJson: any, + commonArgs: Record, + contextVariables: string[], + contextDataloaderName: string | symbol, + headersFactory: HeadersFactory, + config: YamlConfig.ODataHandler, + env: Record, + baseUrl: string, + eventEmitterSet: Set +} + +export function buildGraphQLSchema(args: SchemaBuilderArgs): GraphQLSchema { + const builder = new GraphQLSchemaBuilder(args); + return builder.buildSchema(); +} + +export class GraphQLSchemaBuilder { + metadataJson: any; + commonArgs: Record; + contextVariables: string[]; + contextDataloaderName: string | symbol; + headersFactory: HeadersFactory; + config: YamlConfig.ODataHandler; + baseUrl: string; + eventEmitterSet: Set; + env: Record; + schemaComposer: SchemaComposer; + aliasNamespaceMap: Map; + namespaces: Set; + schemas: any[]; + multipleSchemas: boolean; + + constructor({ + metadataJson, + commonArgs, + contextDataloaderName, + headersFactory, + config, + env, + baseUrl, + eventEmitterSet + }: SchemaBuilderArgs) { + this.baseUrl = baseUrl; + this.env = env; + this.metadataJson = metadataJson; + this.commonArgs = commonArgs; + this.headersFactory = headersFactory; + this.contextDataloaderName = contextDataloaderName; + this.eventEmitterSet = eventEmitterSet; + this.config = config; + + this.schemaComposer = initSchemaComposer(); + this.aliasNamespaceMap = new Map(); + this.schemas = this.metadataJson.Edmx[0].DataServices[0].Schema; + this.namespaces = new Set(); + this.multipleSchemas = this.schemas.length > 1; + + } + + buildSchema(): GraphQLSchema { + this.schemaComposer.createEnumTC({ + name: 'InlineCount', + values: { + allpages: { + value: 'allpages', + description: + 'The OData MUST include a count of the number of entities in the collection identified by the URI (after applying any $filter System Query Options present on the URI)', + }, + none: { + value: 'none', + description: + 'The OData service MUST NOT include a count in the response. This is equivalence to a URI that does not include a $inlinecount query string parameter.', + }, + }, + }); + + this.schemaComposer.createInputTC({ + name: 'QueryOptions', + fields: queryOptionsFields, + }); + + this.schemas?.forEach((schemaObj: any) => { + const schemaNamespace = schemaObj.attributes.Namespace; + this.namespaces.add(schemaNamespace); + const schemaAlias = schemaObj.attributes.Alias; + if (schemaAlias) { + this.aliasNamespaceMap.set(schemaNamespace, schemaAlias); + } + }); + + this.schemas.forEach((schemaObj: any) => { + this.handleSchema(schemaObj); + }); + + // graphql-compose doesn't add @defer and @stream to the schema + specifiedDirectives.forEach(directive => this.schemaComposer.addDirective(directive)); + + const schema = this.schemaComposer.buildSchema(); + return schema; + } + + private getDataLoader(context: any): DataLoader { + return context[this.contextDataloaderName]; + } + + private handleSchema(schemaObj: any) { + const schemaNamespace = schemaObj.attributes.Namespace; + + schemaObj.EnumType?.forEach((enumObj: any) => + this.handleEnum(enumObj, schemaNamespace)); + + const allTypes = (schemaObj.EntityType || []).concat(schemaObj.ComplexType || []); + const typesWithBaseType = allTypes.filter((typeObj: any) => typeObj.attributes.BaseType); + + allTypes?.forEach((typeObj: any) => this.handleType(typeObj, schemaNamespace, typesWithBaseType)); + + schemaObj.Function?.forEach((functionObj: any) => { + if (functionObj.attributes?.IsBound === 'true') { + this.handleBoundFunctionObj(functionObj, schemaNamespace); + } else { + this.handleUnboundFunctionObj(functionObj); + } + }); + + schemaObj.Action?.forEach((actionObj: any) => { + if (actionObj.attributes?.IsBound === 'true') { + this.handleBoundActionObj(actionObj, schemaNamespace); + } else { + this.handleUnboundActionObj(actionObj); + } + }); + + // Rearrange fields for base types and implementations + typesWithBaseType?.forEach((typeObj: any) => this.rearrangeFieldsForType(typeObj, schemaNamespace)); + + schemaObj.EntityContainer?.forEach((entityContainerObj: any) => + this.handleEntityContainer(entityContainerObj)); + } + + private handleEnum(enumObj: any, schemaNamespace: string) { + const values: Record = {}; + enumObj.Member?.forEach((memberObj: any) => { + const key = memberObj.attributes.Name; + // This doesn't work. + // const value = memberElement.getAttribute('Value')!; + values[key] = { + value: key, + extensions: { memberObj }, + }; + }); + const enumTypeName = this.buildName({ schemaNamespace, name: enumObj.attributes.Name }); + this.schemaComposer.createEnumTC({ + name: enumTypeName, + values, + extensions: { enumObj }, + }); + } + + private handleType(typeObj: any, schemaNamespace: string, typesWithBaseType: any[]) { + const entityTypeName = this.buildName({ schemaNamespace, name: typeObj.attributes.Name }); + const isOpenType = typeObj.attributes.OpenType === 'true'; + const isAbstract = typeObj.attributes.Abstract === 'true'; + const eventEmitter = new EventEmitter(); + eventEmitter.setMaxListeners(Infinity); + this.eventEmitterSet.add(eventEmitter); + const extensions: EntityTypeExtensions = { + entityInfo: { + actualFields: [], + navigationFields: [], + isOpenType, + }, + typeObj, + eventEmitter, + }; + const inputType = this.schemaComposer.createInputTC({ + name: entityTypeName + 'Input', + fields: {}, + extensions: () => extensions, + }); + let abstractType: InterfaceTypeComposer; + if ( + typesWithBaseType.some((typeObj: any) => typeObj.attributes.BaseType.includes(`.${entityTypeName}`)) || + isAbstract + ) { + abstractType = this.schemaComposer.createInterfaceTC({ + name: isAbstract ? entityTypeName : `I${entityTypeName}`, + extensions, + resolveType: (root: any) => { + const typeRef = root['@odata.type']?.replace('#', ''); + if (typeRef) { + const typeName = this.getTypeNameFromRef({ + typeRef: root['@odata.type'].replace('#', ''), + isInput: false, + isRequired: false, + }); + return typeName; + } + return isAbstract ? `T${entityTypeName}` : entityTypeName; + }, + }); + } + const outputType = this.schemaComposer.createObjectTC({ + name: isAbstract ? `T${entityTypeName}` : entityTypeName, + extensions, + interfaces: abstractType ? [abstractType] : [], + }); + + abstractType?.setInputTypeComposer(inputType); + outputType.setInputTypeComposer(inputType); + + const propertyRefObj = typeObj.Key && typeObj.Key[0].PropertyRef[0]; + if (propertyRefObj) { + extensions.entityInfo.identifierFieldName = propertyRefObj.attributes.Name; + } + + typeObj.Property?.forEach((propertyObj: any) => { + const propertyName = propertyObj.attributes.Name; + extensions.entityInfo.actualFields.push(propertyName); + const propertyTypeRef = propertyObj.attributes.Type; + if (propertyName === extensions.entityInfo.identifierFieldName) { + extensions.entityInfo.identifierFieldTypeRef = propertyTypeRef; + } + const isRequired = propertyObj.attributes.Nullable === 'false'; + inputType.addFields({ + [propertyName]: { + type: this.getTypeNameFromRef({ + typeRef: propertyTypeRef, + isInput: true, + isRequired, + }), + extensions: { propertyObj }, + }, + }); + const field: ObjectTypeComposerFieldConfigDefinition = { + type: this.getTypeNameFromRef({ + typeRef: propertyTypeRef, + isInput: false, + isRequired, + }), + extensions: { propertyObj }, + }; + abstractType?.addFields({ + [propertyName]: field, + }); + outputType.addFields({ + [propertyName]: field, + }); + }); + typeObj.NavigationProperty?.forEach((navigationPropertyObj: any) => { + const navigationPropertyName = navigationPropertyObj.attributes.Name; + extensions.entityInfo.navigationFields.push(navigationPropertyName); + const navigationPropertyTypeRef = navigationPropertyObj.attributes.Type; + const isRequired = navigationPropertyObj.attributes.Nullable === 'false'; + const isList = navigationPropertyTypeRef.startsWith('Collection('); + if (isList) { + const singularField: ObjectTypeComposerFieldConfigDefinition = { + type: this.getTypeNameFromRef({ + typeRef: navigationPropertyTypeRef, + isInput: false, + isRequired, + }) + .replace('[', '') + .replace(']', ''), + args: { + ...this.commonArgs, + id: { + type: 'ID', + }, + }, + extensions: { navigationPropertyObj }, + resolve: async (root, args, context, info) => { + if (navigationPropertyName in root) { + return root[navigationPropertyName]; + } + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + navigationPropertyName); + const returnType = info.returnType as GraphQLObjectType; + const { entityInfo } = returnType.extensions as EntityTypeExtensions; + addIdentifierToUrl(url, entityInfo.identifierFieldName, entityInfo.identifierFieldTypeRef, args); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, this.config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + const pluralField: ObjectTypeComposerFieldConfigDefinition = { + type: this.getTypeNameFromRef({ + typeRef: navigationPropertyTypeRef, + isInput: false, + isRequired, + }), + args: { + ...this.commonArgs, + queryOptions: { type: 'QueryOptions' }, + }, + extensions: { navigationPropertyObj }, + resolve: async (root, args, context, info) => { + if (navigationPropertyName in root) { + return root[navigationPropertyName]; + } + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + navigationPropertyName); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, this.config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + abstractType?.addFields({ + [navigationPropertyName]: pluralField, + [`${navigationPropertyName}ById`]: singularField, + }); + outputType.addFields({ + [navigationPropertyName]: pluralField, + [`${navigationPropertyName}ById`]: singularField, + }); + } else { + const field: ObjectTypeComposerFieldConfigDefinition = { + type: this.getTypeNameFromRef({ + typeRef: navigationPropertyTypeRef, + isInput: false, + isRequired, + }), + args: { + ...this.commonArgs, + }, + extensions: { navigationPropertyObj }, + resolve: async (root, args, context, info) => { + if (navigationPropertyName in root) { + return root[navigationPropertyName]; + } + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + navigationPropertyName); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, this.config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + abstractType?.addFields({ + [navigationPropertyName]: field, + }); + outputType.addFields({ + [navigationPropertyName]: field, + }); + } + }); + if (isOpenType || outputType.getFieldNames().length === 0) { + extensions.entityInfo.isOpenType = true; + inputType.addFields({ + rest: { + type: 'JSON', + }, + }); + abstractType?.addFields({ + rest: { + type: 'JSON', + resolve: (root: any) => root, + }, + }); + outputType.addFields({ + rest: { + type: 'JSON', + resolve: (root: any) => root, + }, + }); + } + const updateInputType = inputType.clone(`${entityTypeName}UpdateInput`); + updateInputType.getFieldNames()?.forEach(fieldName => updateInputType.makeOptional(fieldName)); + // Types might be considered as unused implementations of interfaces so we must prevent that + this.schemaComposer.addSchemaMustHaveType(outputType); + } + + private rearrangeFieldsForType(typeObj: any, schemaNamespace: string) { + const typeName = this.buildName({ + schemaNamespace, + name: typeObj.attributes.Name, + }); + const inputType = this.schemaComposer.getITC(typeName + 'Input') as InputTypeComposer; + const abstractType = this.getTCByTypeNames('I' + typeName, typeName) as InterfaceTypeComposer; + const outputType = this.getTCByTypeNames('T' + typeName, typeName) as ObjectTypeComposer; + const baseTypeRef = typeObj.attributes.BaseType; + const { entityInfo, eventEmitter } = outputType.getExtensions() as EntityTypeExtensions; + const baseTypeName = this.getTypeNameFromRef({ + typeRef: baseTypeRef, + isInput: false, + isRequired: false, + }); + const baseInputType = this.schemaComposer.getAnyTC(baseTypeName + 'Input') as InputTypeComposer; + const baseAbstractType = this.getTCByTypeNames('I' + baseTypeName, baseTypeName) as InterfaceTypeComposer; + const baseOutputType = this.getTCByTypeNames('T' + baseTypeName, baseTypeName) as ObjectTypeComposer; + const { entityInfo: baseEntityInfo, eventEmitter: baseEventEmitter } = + baseOutputType.getExtensions() as EntityTypeExtensions; + const baseEventEmitterListener = () => { + inputType.addFields(baseInputType.getFields()); + entityInfo.identifierFieldName = baseEntityInfo.identifierFieldName || entityInfo.identifierFieldName; + entityInfo.identifierFieldTypeRef = + baseEntityInfo.identifierFieldTypeRef || entityInfo.identifierFieldTypeRef; + entityInfo.actualFields.unshift(...baseEntityInfo.actualFields); + abstractType?.addFields(baseAbstractType?.getFields()); + outputType.addFields(baseOutputType.getFields()); + if (baseAbstractType instanceof InterfaceTypeComposer) { + // abstractType.addInterface(baseAbstractType.getTypeName()); + outputType.addInterface(baseAbstractType.getTypeName()); + } + eventEmitter.emit('onFieldChange'); + }; + baseEventEmitter.on('onFieldChange', baseEventEmitterListener); + baseEventEmitterListener(); + } + + private handleUnboundFunctionObj(unboundFunctionObj: any) { + const functionName = unboundFunctionObj.attributes.Name; + const returnTypeRef = unboundFunctionObj.ReturnType[0].attributes.Type; + const returnType = this.getTypeNameFromRef({ + typeRef: returnTypeRef, + isInput: false, + isRequired: false, + }); + this.schemaComposer.Query.addFields({ + [functionName]: { + type: returnType, + args: { + ...this.commonArgs, + }, + resolve: async (root, args, context, info) => { + const url = new URL(this.baseUrl); + url.href = urljoin(url.href, '/' + functionName); + url.href += `(${Object.entries(args) + .filter(argEntry => argEntry[0] !== 'queryOptions') + .map(argEntry => argEntry.join(' = ')) + .join(', ')})`; + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, this.config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }); + unboundFunctionObj.Parameter?.forEach((parameterObj: any) => { + const parameterName = parameterObj.attributes.Name; + const parameterTypeRef = parameterObj.attributes.Type; + const isRequired = parameterObj.attributes.Nullable === 'false'; + const parameterType = this.getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: true, + isRequired, + }); + this.schemaComposer.Query.addFieldArgs(functionName, { + [parameterName]: { + type: parameterType, + }, + }); + }); + } + + private handleBoundFunctionObj(boundFunctionObj: any, schemaNamespace: string) { + const functionName = boundFunctionObj.attributes.Name; + const functionRef = schemaNamespace + '.' + functionName; + const returnTypeRef = boundFunctionObj.ReturnType[0].attributes.Type; + const returnType = this.getTypeNameFromRef({ + typeRef: returnTypeRef, + isInput: false, + isRequired: false, + }); + const args: ObjectTypeComposerArgumentConfigMapDefinition = { + ...this.commonArgs, + }; + // eslint-disable-next-line prefer-const + let entitySetPath = boundFunctionObj.attributes.EntitySetPath; + boundFunctionObj.Parameter?.forEach((parameterObj: any) => { + const parameterName = parameterObj.attributes.Name; + const parameterTypeRef = parameterObj.attributes.Type; + const isRequired = parameterObj.attributes.Nullable === 'false'; + const parameterTypeName = this.getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: true, + isRequired, + }); + // If entitySetPath is not available, take first parameter as entity + entitySetPath = entitySetPath || parameterName; + if (entitySetPath === parameterName) { + const boundEntityTypeName = this.getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: false, + isRequired: false, + }) + .replace('[', '') + .replace(']', ''); + const boundEntityType = this.schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer; + const boundEntityOtherType = this.getTCByTypeNames( + 'I' + boundEntityTypeName, + 'T' + boundEntityTypeName + ) as InterfaceTypeComposer; + const field: ObjectTypeComposerFieldConfigDefinition = { + type: returnType, + args, + resolve: async (root, args, context, info) => { + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + functionRef); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, this.config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + boundEntityType.addFields({ + [functionName]: field, + }); + boundEntityOtherType?.addFields({ + [functionName]: field, + }); + } + args[parameterName] = { + type: parameterTypeName, + }; + }); + } + + private handleUnboundActionObj(unboundActionObj: any) { + const actionName = unboundActionObj.attributes.Name; + this.schemaComposer.Mutation.addFields({ + [actionName]: { + type: 'JSON', + args: { + ...this.commonArgs, + }, + resolve: async (root, args, context, info) => { + const url = new URL(this.baseUrl); + url.href = urljoin(url.href, '/' + actionName); + const urlString = getUrlString(url); + const method = 'POST'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + body: jsonFlatStringify(args), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }); + + unboundActionObj.Parameter?.forEach((parameterObj: any) => { + const parameterName = parameterObj.attributes.Name; + const parameterTypeRef = parameterObj.attributes.Type; + const isRequired = parameterObj.attributes.Nullable === 'false'; + const parameterType = this.getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: true, + isRequired, + }); + + this.schemaComposer.Mutation.addFieldArgs(actionName, { + [parameterName]: { + type: parameterType, + }, + }); + }); + } + + private handleBoundActionObj(boundActionObj: any, schemaNamespace: string) { + const actionName = boundActionObj.attributes.Name; + const actionRef = schemaNamespace + '.' + actionName; + const args: ObjectTypeComposerArgumentConfigMapDefinition = { + ...this.commonArgs, + }; + let entitySetPath = boundActionObj.attributes.EntitySetPath; + let boundField: ObjectTypeComposerFieldConfigDefinition; + let boundEntityTypeName: string; + boundActionObj.Parameter?.forEach((parameterObj: any) => { + const parameterName = parameterObj.attributes.Name; + const parameterTypeRef = parameterObj.attributes.Type; + const isRequired = parameterObj.attributes.Nullable === 'false'; + const parameterTypeName = this.getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: true, + isRequired, + }); + // If entitySetPath is not available, take first parameter as entity + entitySetPath = entitySetPath || parameterName; + if (entitySetPath === parameterName) { + boundEntityTypeName = this.getTypeNameFromRef({ + typeRef: parameterTypeRef, + isInput: false, + isRequired: false, + }) + .replace('[', '') + .replace(']', ''); // Todo temp workaround + boundField = { + type: 'JSON', + args, + resolve: async (root, args, context, info) => { + const url = new URL(root['@odata.id']); + url.href = urljoin(url.href, '/' + actionRef); + const urlString = getUrlString(url); + const method = 'POST'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + body: jsonFlatStringify(args), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }; + } + args[parameterName] = { + type: parameterTypeName, + }; + }); + const boundEntityType = this.schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer; + boundEntityType.addFields({ + [actionName]: boundField, + }); + const otherType = this.getTCByTypeNames( + `I${boundEntityTypeName}`, + `T${boundEntityTypeName}` + ) as InterfaceTypeComposer; + otherType?.addFields({ + [actionName]: boundField, + }); + } + + private handleEntityContainer(entityContainerObj: any) { + entityContainerObj.Singleton?.forEach((singletonObj: any) => + this.handleSingleton(singletonObj)); + + entityContainerObj.EntitySet?.forEach((entitySetObj: any) => + this.handleEntitySet(entitySetObj)); + + } + + private handleSingleton(singletonObj: any) { + const singletonName = singletonObj.attributes.Name; + const singletonTypeRef = singletonObj.attributes.Type; + const singletonTypeName = this.getTypeNameFromRef({ + typeRef: singletonTypeRef, + isInput: false, + isRequired: false, + }); + this.schemaComposer.Query.addFields({ + [singletonName]: { + type: singletonTypeName, + args: { + ...this.commonArgs, + }, + resolve: async (root, args, context, info) => { + const url = new URL(this.baseUrl); + url.href = urljoin(url.href, '/' + singletonName); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, this.config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }); + } + + private handleEntitySet(entitySetObj: any) { + const entitySetName = entitySetObj.attributes.Name; + const entitySetTypeRef = entitySetObj.attributes.EntityType; + const entityTypeName = this.getTypeNameFromRef({ + typeRef: entitySetTypeRef, + isInput: false, + isRequired: false, + }); + const entityOutputTC = this.getTCByTypeNames('I' + entityTypeName, entityTypeName) as + | InterfaceTypeComposer + | ObjectTypeComposer; + const { entityInfo } = entityOutputTC.getExtensions() as EntityTypeExtensions; + const identifierFieldName = entityInfo.identifierFieldName; + const identifierFieldTypeRef = entityInfo.identifierFieldTypeRef; + const identifierFieldTypeName = entityOutputTC.getFieldTypeName(identifierFieldName); + const typeName = entityOutputTC.getTypeName(); + const commonFields: Record> = { + [entitySetName]: { + type: `[${typeName}]`, + args: { + ...this.commonArgs, + queryOptions: { type: 'QueryOptions' }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(this.baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, this.config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + [`${entitySetName}By${identifierFieldName}`]: { + type: typeName, + args: { + ...this.commonArgs, + [identifierFieldName]: { + type: identifierFieldTypeName, + }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(this.baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); + const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; + const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, this.config); + searchParams?.forEach((value, key) => { + url.searchParams.set(key, value); + }); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }; + this.schemaComposer.Query.addFields({ + ...commonFields, + [`${entitySetName}Count`]: { + type: 'Int', + args: { + ...this.commonArgs, + queryOptions: { type: 'QueryOptions' }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(this.baseUrl); + url.href = urljoin(url.href, `/${entitySetName}/$count`); + const urlString = getUrlString(url); + const method = 'GET'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return responseText; + }, + }, + }); + this.schemaComposer.Mutation.addFields({ + ...commonFields, + [`create${entitySetName}`]: { + type: typeName, + args: { + ...this.commonArgs, + input: { + type: entityTypeName + 'Input', + }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(this.baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + const urlString = getUrlString(url); + this.rebuildOpenInputObjects(args.input); + const method = 'POST'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + body: jsonFlatStringify(args.input), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + [`delete${entitySetName}By${identifierFieldName}`]: { + type: 'JSON', + args: { + ...this.commonArgs, + [identifierFieldName]: { + type: identifierFieldTypeName, + }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(this.baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); + const urlString = getUrlString(url); + const method = 'DELETE'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + [`update${entitySetName}By${identifierFieldName}`]: { + type: typeName, + args: { + ...this.commonArgs, + [identifierFieldName]: { + type: identifierFieldTypeName, + }, + input: { + type: entityTypeName + 'UpdateInput', + }, + }, + resolve: async (root, args, context, info) => { + const url = new URL(this.baseUrl); + url.href = urljoin(url.href, '/' + entitySetName); + addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); + const urlString = getUrlString(url); + this.rebuildOpenInputObjects(args.input); + const method = 'PATCH'; + const request = new Request(urlString, { + method, + headers: this.headersFactory({ root, args, context, info, env: this.env }, method), + body: jsonFlatStringify(args.input), + }); + const response = await this.getDataLoader(context).load(request); + const responseText = await response.text(); + return handleResponseText(responseText, urlString, info); + }, + }, + }); + } + + private getNamespaceFromTypeRef(typeRef: string) { + let namespace = ''; + this.namespaces?.forEach(el => { + if ( + typeRef.startsWith(el) && + el.length > namespace.length && // It can be deeper namespace + !typeRef.replace(el + '.', '').includes('.') // Typename cannot have `.` + ) { + namespace = el; + } + }); + return namespace; + } + + private getTypeNameFromRef({ + typeRef, + isInput, + isRequired, + }: { + typeRef: string; + isInput: boolean; + isRequired: boolean; + }) { + const typeRefArr = typeRef.split('Collection('); + const arrayDepth = typeRefArr.length; + let actualTypeRef = typeRefArr.join('').split(')').join(''); + const typeNamespace = this.getNamespaceFromTypeRef(actualTypeRef); + if (this.aliasNamespaceMap.has(typeNamespace)) { + const alias = this.aliasNamespaceMap.get(typeNamespace); + actualTypeRef = actualTypeRef.replace(typeNamespace, alias); + } + const actualTypeRefArr = actualTypeRef.split('.'); + const typeName = this.multipleSchemas + ? pascalCase(actualTypeRefArr.join('_')) + : actualTypeRefArr[actualTypeRefArr.length - 1]; + let realTypeName = typeName; + if (SCALARS.has(actualTypeRef)) { + realTypeName = SCALARS.get(actualTypeRef); + } else if (this.schemaComposer.isEnumType(typeName)) { + realTypeName = typeName; + } else if (isInput) { + realTypeName += 'Input'; + } + const fakeEmptyArr = new Array(arrayDepth); + realTypeName = fakeEmptyArr.join('[') + realTypeName + fakeEmptyArr.join(']'); + if (isRequired) { + realTypeName += '!'; + } + return realTypeName; + } + + private getTCByTypeNames(...typeNames: string[]) { + for (const typeName of typeNames) { + try { + return this.schemaComposer.getAnyTC(typeName); + } catch {} + } + return null; + } + + private rebuildOpenInputObjects(input: any) { + if (typeof input === 'object') { + if ('rest' in input) { + Object.assign(input, input.rest); + delete input.rest; + } + for (const fieldName in input) { + this.rebuildOpenInputObjects(input[fieldName]); + } + } + } + + private buildName({ schemaNamespace, name }: { schemaNamespace: string; name: string }) { + const alias = this.aliasNamespaceMap.get(schemaNamespace) || schemaNamespace; + const ref = alias + '.' + name; + return this.multipleSchemas ? pascalCase(ref.split('.').join('_')) : name; + } +} + +function initSchemaComposer() { + const schemaComposer = new SchemaComposer(); + schemaComposer.add(GraphQLBigInt); + schemaComposer.add(GraphQLGUID); + schemaComposer.add(GraphQLDateTime); + schemaComposer.add(GraphQLJSON); + schemaComposer.add(GraphQLByte); + schemaComposer.add(GraphQLDate); + schemaComposer.add(GraphQLISO8601Duration); + + return schemaComposer; +} + +function prepareSearchParams(fragment: ResolveTree, schema: GraphQLSchema, config: YamlConfig.ODataHandler) { + const fragmentTypeNames = Object.keys(fragment.fieldsByTypeName) as string[]; + const returnType = schema.getType(fragmentTypeNames[0]); + const { args, fields } = simplifyParsedResolveInfoFragmentWithType(fragment, returnType); + const searchParams = new URLSearchParams(); + if ('queryOptions' in args) { + const { queryOptions } = args as any; + for (const param in queryOptionsFields) { + if (param in queryOptions) { + searchParams.set('$' + param, queryOptions[param]); + } + } + } + + // $select doesn't work with inherited types' fields. So if there is an inline fragment for + // implemented types, we cannot use $select + const isSelectable = !isAbstractType(returnType); + + if (isSelectable) { + const { entityInfo } = returnType.extensions as EntityTypeExtensions; + const selectionFields: string[] = []; + const expandedFields: string[] = []; + for (const fieldName in fields) { + if (entityInfo.actualFields.includes(fieldName)) { + selectionFields.push(fieldName); + } + if (config.expandNavProps && entityInfo.navigationFields.includes(fieldName)) { + const searchParams = prepareSearchParams(fields[fieldName], schema, config); + const searchParamsStr = decodeURIComponent(searchParams.toString()); + expandedFields.push(`${fieldName}(${searchParamsStr.split('&').join(';')})`); + selectionFields.push(fieldName); + } + } + if (!selectionFields.includes(entityInfo.identifierFieldName)) { + selectionFields.push(entityInfo.identifierFieldName); + } + if (selectionFields.length) { + searchParams.set('$select', selectionFields.join(',')); + } + if (expandedFields.length) { + searchParams.set('$expand', expandedFields.join(',')); + } + } + return searchParams; +} diff --git a/packages/handlers/odata/src/schema-generation.ts b/packages/handlers/odata/src/schema-generation.ts deleted file mode 100644 index 0da414ce4ef7f..0000000000000 --- a/packages/handlers/odata/src/schema-generation.ts +++ /dev/null @@ -1,1016 +0,0 @@ -import { EventEmitter } from 'events'; -import { - isAbstractType, - GraphQLObjectType, - GraphQLSchema, - specifiedDirectives, -} from 'graphql'; -import { - SchemaComposer, - ObjectTypeComposer, - InterfaceTypeComposer, - ObjectTypeComposerFieldConfigDefinition, - ObjectTypeComposerArgumentConfigMapDefinition, - EnumTypeComposerValueConfigDefinition, - InputTypeComposer, -} from 'graphql-compose'; -import { - GraphQLBigInt, - GraphQLGUID, - GraphQLDateTime, - GraphQLJSON, - GraphQLDate, - GraphQLByte, - GraphQLISO8601Duration, -} from 'graphql-scalars'; -import { jsonFlatStringify } from "@graphql-mesh/utils"; -import { YamlConfig } from '@graphql-mesh/types'; -import { parseResolveInfo, ResolveTree, simplifyParsedResolveInfoFragmentWithType } from 'graphql-parse-resolve-info'; -import urljoin from 'url-join'; -import { pascalCase } from 'pascal-case'; -import { Request } from 'cross-fetch'; -import { SCALARS } from './scalars'; -import { queryOptionsFields } from './query-options'; -import { EntityTypeExtensions } from './schema-util'; -import { addIdentifierToUrl, getUrlString } from './util'; -import { handleResponseText, HeadersFactory } from './request-processing' - -type GenerateSchemaArgs = { - metadataJson: any, - commonArgs: Record, - contextVariables: string[], - contextDataloaderName: string | symbol, - headersFactory: HeadersFactory, - config: YamlConfig.ODataHandler, - env: Record, - baseUrl: string, - eventEmitterSet: Set -} - -export function generateGraphQLSchema({ - metadataJson, - commonArgs, - contextVariables, - contextDataloaderName, - headersFactory, - config, - env, - baseUrl, - eventEmitterSet -}: GenerateSchemaArgs): GraphQLSchema { - const schemaComposer = initSchemaComposer(); - - const aliasNamespaceMap = new Map(); - - const schemas = metadataJson.Edmx[0].DataServices[0].Schema; - const multipleSchemas = schemas.length > 1; - const namespaces = new Set(); - - function getNamespaceFromTypeRef(typeRef: string) { - let namespace = ''; - namespaces?.forEach(el => { - if ( - typeRef.startsWith(el) && - el.length > namespace.length && // It can be deeper namespace - !typeRef.replace(el + '.', '').includes('.') // Typename cannot have `.` - ) { - namespace = el; - } - }); - return namespace; - } - - function getTypeNameFromRef({ - typeRef, - isInput, - isRequired, - }: { - typeRef: string; - isInput: boolean; - isRequired: boolean; - }) { - const typeRefArr = typeRef.split('Collection('); - const arrayDepth = typeRefArr.length; - let actualTypeRef = typeRefArr.join('').split(')').join(''); - const typeNamespace = getNamespaceFromTypeRef(actualTypeRef); - if (aliasNamespaceMap.has(typeNamespace)) { - const alias = aliasNamespaceMap.get(typeNamespace); - actualTypeRef = actualTypeRef.replace(typeNamespace, alias); - } - const actualTypeRefArr = actualTypeRef.split('.'); - const typeName = multipleSchemas - ? pascalCase(actualTypeRefArr.join('_')) - : actualTypeRefArr[actualTypeRefArr.length - 1]; - let realTypeName = typeName; - if (SCALARS.has(actualTypeRef)) { - realTypeName = SCALARS.get(actualTypeRef); - } else if (schemaComposer.isEnumType(typeName)) { - realTypeName = typeName; - } else if (isInput) { - realTypeName += 'Input'; - } - const fakeEmptyArr = new Array(arrayDepth); - realTypeName = fakeEmptyArr.join('[') + realTypeName + fakeEmptyArr.join(']'); - if (isRequired) { - realTypeName += '!'; - } - return realTypeName; - } - - schemaComposer.createEnumTC({ - name: 'InlineCount', - values: { - allpages: { - value: 'allpages', - description: - 'The OData MUST include a count of the number of entities in the collection identified by the URI (after applying any $filter System Query Options present on the URI)', - }, - none: { - value: 'none', - description: - 'The OData service MUST NOT include a count in the response. This is equivalence to a URI that does not include a $inlinecount query string parameter.', - }, - }, - }); - - schemaComposer.createInputTC({ - name: 'QueryOptions', - fields: queryOptionsFields, - }); - - function getTCByTypeNames(...typeNames: string[]) { - for (const typeName of typeNames) { - try { - return schemaComposer.getAnyTC(typeName); - } catch {} - } - return null; - } - - function rebuildOpenInputObjects(input: any) { - if (typeof input === 'object') { - if ('rest' in input) { - Object.assign(input, input.rest); - delete input.rest; - } - for (const fieldName in input) { - rebuildOpenInputObjects(input[fieldName]); - } - } - } - - function buildName({ schemaNamespace, name }: { schemaNamespace: string; name: string }) { - const alias = aliasNamespaceMap.get(schemaNamespace) || schemaNamespace; - const ref = alias + '.' + name; - return multipleSchemas ? pascalCase(ref.split('.').join('_')) : name; - } - - schemas?.forEach((schemaObj: any) => { - const schemaNamespace = schemaObj.attributes.Namespace; - namespaces.add(schemaNamespace); - const schemaAlias = schemaObj.attributes.Alias; - if (schemaAlias) { - aliasNamespaceMap.set(schemaNamespace, schemaAlias); - } - }); - - // start schemas.forEach loop - schemas?.forEach((schemaObj: any) => { - const schemaNamespace = schemaObj.attributes.Namespace; - - schemaObj.EnumType?.forEach((enumObj: any) => { - const values: Record = {}; - enumObj.Member?.forEach((memberObj: any) => { - const key = memberObj.attributes.Name; - // This doesn't work. - // const value = memberElement.getAttribute('Value')!; - values[key] = { - value: key, - extensions: { memberObj }, - }; - }); - const enumTypeName = buildName({ schemaNamespace, name: enumObj.attributes.Name }); - schemaComposer.createEnumTC({ - name: enumTypeName, - values, - extensions: { enumObj }, - }); - }); - - const allTypes = (schemaObj.EntityType || []).concat(schemaObj.ComplexType || []); - const typesWithBaseType = allTypes.filter((typeObj: any) => typeObj.attributes.BaseType); - - allTypes?.forEach((typeObj: any) => { - const entityTypeName = buildName({ schemaNamespace, name: typeObj.attributes.Name }); - const isOpenType = typeObj.attributes.OpenType === 'true'; - const isAbstract = typeObj.attributes.Abstract === 'true'; - const eventEmitter = new EventEmitter(); - eventEmitter.setMaxListeners(Infinity); - eventEmitterSet.add(eventEmitter); - const extensions: EntityTypeExtensions = { - entityInfo: { - actualFields: [], - navigationFields: [], - isOpenType, - }, - typeObj, - eventEmitter, - }; - const inputType = schemaComposer.createInputTC({ - name: entityTypeName + 'Input', - fields: {}, - extensions: () => extensions, - }); - let abstractType: InterfaceTypeComposer; - if ( - typesWithBaseType.some((typeObj: any) => typeObj.attributes.BaseType.includes(`.${entityTypeName}`)) || - isAbstract - ) { - abstractType = schemaComposer.createInterfaceTC({ - name: isAbstract ? entityTypeName : `I${entityTypeName}`, - extensions, - resolveType: (root: any) => { - const typeRef = root['@odata.type']?.replace('#', ''); - if (typeRef) { - const typeName = getTypeNameFromRef({ - typeRef: root['@odata.type'].replace('#', ''), - isInput: false, - isRequired: false, - }); - return typeName; - } - return isAbstract ? `T${entityTypeName}` : entityTypeName; - }, - }); - } - const outputType = schemaComposer.createObjectTC({ - name: isAbstract ? `T${entityTypeName}` : entityTypeName, - extensions, - interfaces: abstractType ? [abstractType] : [], - }); - - abstractType?.setInputTypeComposer(inputType); - outputType.setInputTypeComposer(inputType); - - const propertyRefObj = typeObj.Key && typeObj.Key[0].PropertyRef[0]; - if (propertyRefObj) { - extensions.entityInfo.identifierFieldName = propertyRefObj.attributes.Name; - } - - typeObj.Property?.forEach((propertyObj: any) => { - const propertyName = propertyObj.attributes.Name; - extensions.entityInfo.actualFields.push(propertyName); - const propertyTypeRef = propertyObj.attributes.Type; - if (propertyName === extensions.entityInfo.identifierFieldName) { - extensions.entityInfo.identifierFieldTypeRef = propertyTypeRef; - } - const isRequired = propertyObj.attributes.Nullable === 'false'; - inputType.addFields({ - [propertyName]: { - type: getTypeNameFromRef({ - typeRef: propertyTypeRef, - isInput: true, - isRequired, - }), - extensions: { propertyObj }, - }, - }); - const field: ObjectTypeComposerFieldConfigDefinition = { - type: getTypeNameFromRef({ - typeRef: propertyTypeRef, - isInput: false, - isRequired, - }), - extensions: { propertyObj }, - }; - abstractType?.addFields({ - [propertyName]: field, - }); - outputType.addFields({ - [propertyName]: field, - }); - }); - typeObj.NavigationProperty?.forEach((navigationPropertyObj: any) => { - const navigationPropertyName = navigationPropertyObj.attributes.Name; - extensions.entityInfo.navigationFields.push(navigationPropertyName); - const navigationPropertyTypeRef = navigationPropertyObj.attributes.Type; - const isRequired = navigationPropertyObj.attributes.Nullable === 'false'; - const isList = navigationPropertyTypeRef.startsWith('Collection('); - if (isList) { - const singularField: ObjectTypeComposerFieldConfigDefinition = { - type: getTypeNameFromRef({ - typeRef: navigationPropertyTypeRef, - isInput: false, - isRequired, - }) - .replace('[', '') - .replace(']', ''), - args: { - ...commonArgs, - id: { - type: 'ID', - }, - }, - extensions: { navigationPropertyObj }, - resolve: async (root, args, context, info) => { - if (navigationPropertyName in root) { - return root[navigationPropertyName]; - } - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + navigationPropertyName); - const returnType = info.returnType as GraphQLObjectType; - const { entityInfo } = returnType.extensions as EntityTypeExtensions; - addIdentifierToUrl(url, entityInfo.identifierFieldName, entityInfo.identifierFieldTypeRef, args); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - const pluralField: ObjectTypeComposerFieldConfigDefinition = { - type: getTypeNameFromRef({ - typeRef: navigationPropertyTypeRef, - isInput: false, - isRequired, - }), - args: { - ...commonArgs, - queryOptions: { type: 'QueryOptions' }, - }, - extensions: { navigationPropertyObj }, - resolve: async (root, args, context, info) => { - if (navigationPropertyName in root) { - return root[navigationPropertyName]; - } - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + navigationPropertyName); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - abstractType?.addFields({ - [navigationPropertyName]: pluralField, - [`${navigationPropertyName}ById`]: singularField, - }); - outputType.addFields({ - [navigationPropertyName]: pluralField, - [`${navigationPropertyName}ById`]: singularField, - }); - } else { - const field: ObjectTypeComposerFieldConfigDefinition = { - type: getTypeNameFromRef({ - typeRef: navigationPropertyTypeRef, - isInput: false, - isRequired, - }), - args: { - ...commonArgs, - }, - extensions: { navigationPropertyObj }, - resolve: async (root, args, context, info) => { - if (navigationPropertyName in root) { - return root[navigationPropertyName]; - } - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + navigationPropertyName); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - abstractType?.addFields({ - [navigationPropertyName]: field, - }); - outputType.addFields({ - [navigationPropertyName]: field, - }); - } - }); - if (isOpenType || outputType.getFieldNames().length === 0) { - extensions.entityInfo.isOpenType = true; - inputType.addFields({ - rest: { - type: 'JSON', - }, - }); - abstractType?.addFields({ - rest: { - type: 'JSON', - resolve: (root: any) => root, - }, - }); - outputType.addFields({ - rest: { - type: 'JSON', - resolve: (root: any) => root, - }, - }); - } - const updateInputType = inputType.clone(`${entityTypeName}UpdateInput`); - updateInputType.getFieldNames()?.forEach(fieldName => updateInputType.makeOptional(fieldName)); - // Types might be considered as unused implementations of interfaces so we must prevent that - schemaComposer.addSchemaMustHaveType(outputType); - }); - - const handleUnboundFunctionObj = (unboundFunctionObj: any) => { - const functionName = unboundFunctionObj.attributes.Name; - const returnTypeRef = unboundFunctionObj.ReturnType[0].attributes.Type; - const returnType = getTypeNameFromRef({ - typeRef: returnTypeRef, - isInput: false, - isRequired: false, - }); - schemaComposer.Query.addFields({ - [functionName]: { - type: returnType, - args: { - ...commonArgs, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + functionName); - url.href += `(${Object.entries(args) - .filter(argEntry => argEntry[0] !== 'queryOptions') - .map(argEntry => argEntry.join(' = ')) - .join(', ')})`; - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }); - unboundFunctionObj.Parameter?.forEach((parameterObj: any) => { - const parameterName = parameterObj.attributes.Name; - const parameterTypeRef = parameterObj.attributes.Type; - const isRequired = parameterObj.attributes.Nullable === 'false'; - const parameterType = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: true, - isRequired, - }); - schemaComposer.Query.addFieldArgs(functionName, { - [parameterName]: { - type: parameterType, - }, - }); - }); - }; - - const handleBoundFunctionObj = (boundFunctionObj: any) => { - const functionName = boundFunctionObj.attributes.Name; - const functionRef = schemaNamespace + '.' + functionName; - const returnTypeRef = boundFunctionObj.ReturnType[0].attributes.Type; - const returnType = getTypeNameFromRef({ - typeRef: returnTypeRef, - isInput: false, - isRequired: false, - }); - const args: ObjectTypeComposerArgumentConfigMapDefinition = { - ...commonArgs, - }; - // eslint-disable-next-line prefer-const - let entitySetPath = boundFunctionObj.attributes.EntitySetPath; - boundFunctionObj.Parameter?.forEach((parameterObj: any) => { - const parameterName = parameterObj.attributes.Name; - const parameterTypeRef = parameterObj.attributes.Type; - const isRequired = parameterObj.attributes.Nullable === 'false'; - const parameterTypeName = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: true, - isRequired, - }); - // If entitySetPath is not available, take first parameter as entity - entitySetPath = entitySetPath || parameterName; - if (entitySetPath === parameterName) { - const boundEntityTypeName = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: false, - isRequired: false, - }) - .replace('[', '') - .replace(']', ''); - const boundEntityType = schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer; - const boundEntityOtherType = getTCByTypeNames( - 'I' + boundEntityTypeName, - 'T' + boundEntityTypeName - ) as InterfaceTypeComposer; - const field: ObjectTypeComposerFieldConfigDefinition = { - type: returnType, - args, - resolve: async (root, args, context, info) => { - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + functionRef); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - boundEntityType.addFields({ - [functionName]: field, - }); - boundEntityOtherType?.addFields({ - [functionName]: field, - }); - } - args[parameterName] = { - type: parameterTypeName, - }; - }); - }; - - schemaObj.Function?.forEach((functionObj: any) => { - if (functionObj.attributes?.IsBound === 'true') { - handleBoundFunctionObj(functionObj); - } else { - handleUnboundFunctionObj(functionObj); - } - }); - - const handleUnboundActionObj = (unboundActionObj: any) => { - const actionName = unboundActionObj.attributes.Name; - schemaComposer.Mutation.addFields({ - [actionName]: { - type: 'JSON', - args: { - ...commonArgs, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + actionName); - const urlString = getUrlString(url); - const method = 'POST'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - body: jsonFlatStringify(args), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }); - - unboundActionObj.Parameter?.forEach((parameterObj: any) => { - const parameterName = parameterObj.attributes.Name; - const parameterTypeRef = parameterObj.attributes.Type; - const isRequired = parameterObj.attributes.Nullable === 'false'; - const parameterType = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: true, - isRequired, - }); - schemaComposer.Mutation.addFieldArgs(actionName, { - [parameterName]: { - type: parameterType, - }, - }); - }); - }; - - const handleBoundActionObj = (boundActionObj: any) => { - const actionName = boundActionObj.attributes.Name; - const actionRef = schemaNamespace + '.' + actionName; - const args: ObjectTypeComposerArgumentConfigMapDefinition = { - ...commonArgs, - }; - let entitySetPath = boundActionObj.attributes.EntitySetPath; - let boundField: ObjectTypeComposerFieldConfigDefinition; - let boundEntityTypeName: string; - boundActionObj.Parameter?.forEach((parameterObj: any) => { - const parameterName = parameterObj.attributes.Name; - const parameterTypeRef = parameterObj.attributes.Type; - const isRequired = parameterObj.attributes.Nullable === 'false'; - const parameterTypeName = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: true, - isRequired, - }); - // If entitySetPath is not available, take first parameter as entity - entitySetPath = entitySetPath || parameterName; - if (entitySetPath === parameterName) { - boundEntityTypeName = getTypeNameFromRef({ - typeRef: parameterTypeRef, - isInput: false, - isRequired: false, - }) - .replace('[', '') - .replace(']', ''); // Todo temp workaround - boundField = { - type: 'JSON', - args, - resolve: async (root, args, context, info) => { - const url = new URL(root['@odata.id']); - url.href = urljoin(url.href, '/' + actionRef); - const urlString = getUrlString(url); - const method = 'POST'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - body: jsonFlatStringify(args), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }; - } - args[parameterName] = { - type: parameterTypeName, - }; - }); - const boundEntityType = schemaComposer.getAnyTC(boundEntityTypeName) as InterfaceTypeComposer; - boundEntityType.addFields({ - [actionName]: boundField, - }); - const otherType = getTCByTypeNames( - `I${boundEntityTypeName}`, - `T${boundEntityTypeName}` - ) as InterfaceTypeComposer; - otherType?.addFields({ - [actionName]: boundField, - }); - }; - - schemaObj.Action?.forEach((actionObj: any) => { - if (actionObj.attributes?.IsBound === 'true') { - handleBoundActionObj(actionObj); - } else { - handleUnboundActionObj(actionObj); - } - }); - - // Rearrange fields for base types and implementations - typesWithBaseType?.forEach((typeObj: any) => { - const typeName = buildName({ - schemaNamespace, - name: typeObj.attributes.Name, - }); - const inputType = schemaComposer.getITC(typeName + 'Input') as InputTypeComposer; - const abstractType = getTCByTypeNames('I' + typeName, typeName) as InterfaceTypeComposer; - const outputType = getTCByTypeNames('T' + typeName, typeName) as ObjectTypeComposer; - const baseTypeRef = typeObj.attributes.BaseType; - const { entityInfo, eventEmitter } = outputType.getExtensions() as EntityTypeExtensions; - const baseTypeName = getTypeNameFromRef({ - typeRef: baseTypeRef, - isInput: false, - isRequired: false, - }); - const baseInputType = schemaComposer.getAnyTC(baseTypeName + 'Input') as InputTypeComposer; - const baseAbstractType = getTCByTypeNames('I' + baseTypeName, baseTypeName) as InterfaceTypeComposer; - const baseOutputType = getTCByTypeNames('T' + baseTypeName, baseTypeName) as ObjectTypeComposer; - const { entityInfo: baseEntityInfo, eventEmitter: baseEventEmitter } = - baseOutputType.getExtensions() as EntityTypeExtensions; - const baseEventEmitterListener = () => { - inputType.addFields(baseInputType.getFields()); - entityInfo.identifierFieldName = baseEntityInfo.identifierFieldName || entityInfo.identifierFieldName; - entityInfo.identifierFieldTypeRef = - baseEntityInfo.identifierFieldTypeRef || entityInfo.identifierFieldTypeRef; - entityInfo.actualFields.unshift(...baseEntityInfo.actualFields); - abstractType?.addFields(baseAbstractType?.getFields()); - outputType.addFields(baseOutputType.getFields()); - if (baseAbstractType instanceof InterfaceTypeComposer) { - // abstractType.addInterface(baseAbstractType.getTypeName()); - outputType.addInterface(baseAbstractType.getTypeName()); - } - eventEmitter.emit('onFieldChange'); - }; - baseEventEmitter.on('onFieldChange', baseEventEmitterListener); - baseEventEmitterListener(); - }); - - schemaObj.EntityContainer?.forEach((entityContainerObj: any) => { - entityContainerObj.Singleton?.forEach((singletonObj: any) => { - const singletonName = singletonObj.attributes.Name; - const singletonTypeRef = singletonObj.attributes.Type; - const singletonTypeName = getTypeNameFromRef({ - typeRef: singletonTypeRef, - isInput: false, - isRequired: false, - }); - schemaComposer.Query.addFields({ - [singletonName]: { - type: singletonTypeName, - args: { - ...commonArgs, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + singletonName); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }); - }); - - entityContainerObj?.EntitySet?.forEach((entitySetObj: any) => { - const entitySetName = entitySetObj.attributes.Name; - const entitySetTypeRef = entitySetObj.attributes.EntityType; - const entityTypeName = getTypeNameFromRef({ - typeRef: entitySetTypeRef, - isInput: false, - isRequired: false, - }); - const entityOutputTC = getTCByTypeNames('I' + entityTypeName, entityTypeName) as - | InterfaceTypeComposer - | ObjectTypeComposer; - const { entityInfo } = entityOutputTC.getExtensions() as EntityTypeExtensions; - const identifierFieldName = entityInfo.identifierFieldName; - const identifierFieldTypeRef = entityInfo.identifierFieldTypeRef; - const identifierFieldTypeName = entityOutputTC.getFieldTypeName(identifierFieldName); - const typeName = entityOutputTC.getTypeName(); - const commonFields: Record> = { - [entitySetName]: { - type: `[${typeName}]`, - args: { - ...commonArgs, - queryOptions: { type: 'QueryOptions' }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - [`${entitySetName}By${identifierFieldName}`]: { - type: typeName, - args: { - ...commonArgs, - [identifierFieldName]: { - type: identifierFieldTypeName, - }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); - const parsedInfoFragment = parseResolveInfo(info) as ResolveTree; - const searchParams = prepareSearchParams(parsedInfoFragment, info.schema, config); - searchParams?.forEach((value, key) => { - url.searchParams.set(key, value); - }); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }; - schemaComposer.Query.addFields({ - ...commonFields, - [`${entitySetName}Count`]: { - type: 'Int', - args: { - ...commonArgs, - queryOptions: { type: 'QueryOptions' }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, `/${entitySetName}/$count`); - const urlString = getUrlString(url); - const method = 'GET'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return responseText; - }, - }, - }); - schemaComposer.Mutation.addFields({ - ...commonFields, - [`create${entitySetName}`]: { - type: typeName, - args: { - ...commonArgs, - input: { - type: entityTypeName + 'Input', - }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - const urlString = getUrlString(url); - rebuildOpenInputObjects(args.input); - const method = 'POST'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - body: jsonFlatStringify(args.input), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - [`delete${entitySetName}By${identifierFieldName}`]: { - type: 'JSON', - args: { - ...commonArgs, - [identifierFieldName]: { - type: identifierFieldTypeName, - }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); - const urlString = getUrlString(url); - const method = 'DELETE'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - [`update${entitySetName}By${identifierFieldName}`]: { - type: typeName, - args: { - ...commonArgs, - [identifierFieldName]: { - type: identifierFieldTypeName, - }, - input: { - type: entityTypeName + 'UpdateInput', - }, - }, - resolve: async (root, args, context, info) => { - const url = new URL(baseUrl); - url.href = urljoin(url.href, '/' + entitySetName); - addIdentifierToUrl(url, identifierFieldName, identifierFieldTypeRef, args); - const urlString = getUrlString(url); - rebuildOpenInputObjects(args.input); - const method = 'PATCH'; - const request = new Request(urlString, { - method, - headers: headersFactory({ root, args, context, info, env }, method), - body: jsonFlatStringify(args.input), - }); - const response = await context[contextDataloaderName].load(request); - const responseText = await response.text(); - return handleResponseText(responseText, urlString, info); - }, - }, - }); - }); - }); - }); - // end schemas.forEach loop - - // graphql-compose doesn't add @defer and @stream to the schema - specifiedDirectives.forEach(directive => schemaComposer.addDirective(directive)); - - const schema = schemaComposer.buildSchema(); - return schema; -} - - -function initSchemaComposer() { - const schemaComposer = new SchemaComposer(); - schemaComposer.add(GraphQLBigInt); - schemaComposer.add(GraphQLGUID); - schemaComposer.add(GraphQLDateTime); - schemaComposer.add(GraphQLJSON); - schemaComposer.add(GraphQLByte); - schemaComposer.add(GraphQLDate); - schemaComposer.add(GraphQLISO8601Duration); - - return schemaComposer; -} - -function prepareSearchParams(fragment: ResolveTree, schema: GraphQLSchema, config: YamlConfig.ODataHandler) { - const fragmentTypeNames = Object.keys(fragment.fieldsByTypeName) as string[]; - const returnType = schema.getType(fragmentTypeNames[0]); - const { args, fields } = simplifyParsedResolveInfoFragmentWithType(fragment, returnType); - const searchParams = new URLSearchParams(); - if ('queryOptions' in args) { - const { queryOptions } = args as any; - for (const param in queryOptionsFields) { - if (param in queryOptions) { - searchParams.set('$' + param, queryOptions[param]); - } - } - } - - // $select doesn't work with inherited types' fields. So if there is an inline fragment for - // implemented types, we cannot use $select - const isSelectable = !isAbstractType(returnType); - - if (isSelectable) { - const { entityInfo } = returnType.extensions as EntityTypeExtensions; - const selectionFields: string[] = []; - const expandedFields: string[] = []; - for (const fieldName in fields) { - if (entityInfo.actualFields.includes(fieldName)) { - selectionFields.push(fieldName); - } - if (config.expandNavProps && entityInfo.navigationFields.includes(fieldName)) { - const searchParams = prepareSearchParams(fields[fieldName], schema, config); - const searchParamsStr = decodeURIComponent(searchParams.toString()); - expandedFields.push(`${fieldName}(${searchParamsStr.split('&').join(';')})`); - selectionFields.push(fieldName); - } - } - if (!selectionFields.includes(entityInfo.identifierFieldName)) { - selectionFields.push(entityInfo.identifierFieldName); - } - if (selectionFields.length) { - searchParams.set('$select', selectionFields.join(',')); - } - if (expandedFields.length) { - searchParams.set('$expand', expandedFields.join(',')); - } - } - return searchParams; -} From cc5cb7ab8cfc7b225eea4fe7270c8b53973d17cc Mon Sep 17 00:00:00 2001 From: Clement Habinshuti Date: Thu, 15 Jul 2021 19:39:28 +0300 Subject: [PATCH 5/6] Rename util.ts to utils.ts in OData handler --- packages/handlers/odata/src/request-processing.ts | 4 ++-- packages/handlers/odata/src/schema-builder.ts | 4 ++-- .../handlers/odata/src/{schema-util.ts => schema-utils.ts} | 0 packages/handlers/odata/src/{util.ts => utils.ts} | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) rename packages/handlers/odata/src/{schema-util.ts => schema-utils.ts} (100%) rename packages/handlers/odata/src/{util.ts => utils.ts} (84%) diff --git a/packages/handlers/odata/src/request-processing.ts b/packages/handlers/odata/src/request-processing.ts index f4a692cb71225..d0710b22b77a3 100644 --- a/packages/handlers/odata/src/request-processing.ts +++ b/packages/handlers/odata/src/request-processing.ts @@ -4,8 +4,8 @@ import { parseResponse } from 'http-string-parser'; import { getCachedFetch, jsonFlatStringify } from "@graphql-mesh/utils"; import { Request, Response } from 'cross-fetch'; import { nativeFetch } from './native-fetch'; -import { EntityTypeExtensions } from "./schema-util"; -import { getUrlString, addIdentifierToUrl } from "./util"; +import { EntityTypeExtensions } from "./schema-utils"; +import { getUrlString, addIdentifierToUrl } from "./utils"; import urljoin from 'url-join'; import { ResolverData } from "@graphql-mesh/types"; diff --git a/packages/handlers/odata/src/schema-builder.ts b/packages/handlers/odata/src/schema-builder.ts index cdec707a2cf99..19a1205cae4b1 100644 --- a/packages/handlers/odata/src/schema-builder.ts +++ b/packages/handlers/odata/src/schema-builder.ts @@ -31,8 +31,8 @@ import { pascalCase } from 'pascal-case'; import { Request } from 'cross-fetch'; import { SCALARS } from './scalars'; import { queryOptionsFields } from './query-options'; -import { EntityTypeExtensions } from './schema-util'; -import { addIdentifierToUrl, getUrlString } from './util'; +import { EntityTypeExtensions } from './schema-utils'; +import { addIdentifierToUrl, getUrlString } from './utils'; import { handleResponseText, HeadersFactory } from './request-processing' import DataLoader from 'dataloader'; diff --git a/packages/handlers/odata/src/schema-util.ts b/packages/handlers/odata/src/schema-utils.ts similarity index 100% rename from packages/handlers/odata/src/schema-util.ts rename to packages/handlers/odata/src/schema-utils.ts diff --git a/packages/handlers/odata/src/util.ts b/packages/handlers/odata/src/utils.ts similarity index 84% rename from packages/handlers/odata/src/util.ts rename to packages/handlers/odata/src/utils.ts index 7f2a6984ca3ea..d29b8209ff8e4 100644 --- a/packages/handlers/odata/src/util.ts +++ b/packages/handlers/odata/src/utils.ts @@ -1,5 +1,3 @@ -import { ResolverData } from "@graphql-mesh/types"; - export function getUrlString(url: URL) { return decodeURIComponent(url.toString()).split('+').join(' '); } From 233e3755c94b24d716ad7dc799b1aa7e0ced0771 Mon Sep 17 00:00:00 2001 From: Clement Habinshuti Date: Fri, 16 Jul 2021 12:05:19 +0300 Subject: [PATCH 6/6] Fix lint errors --- packages/handlers/odata/src/request-processing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/handlers/odata/src/request-processing.ts b/packages/handlers/odata/src/request-processing.ts index d0710b22b77a3..d3715e0034c02 100644 --- a/packages/handlers/odata/src/request-processing.ts +++ b/packages/handlers/odata/src/request-processing.ts @@ -108,7 +108,7 @@ function initDataloaderFactories(baseUrl: string, env: Record, h (requests: Request[]): Promise => Promise.all(requests.map(request => fetch(request))) ), }; -}; +} export function handleBatchJsonResults(batchResponseJson: any, requests: Request[]) { if ('error' in batchResponseJson) {