graphql#ExecutionResult TypeScript Examples
The following examples show how to use
graphql#ExecutionResult.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.ts From graphql-mesh with MIT License | 6 votes |
getFallbackExecutor(executors: MeshSource['executor'][]): MeshSource['executor'] {
return async function fallbackExecutor(params: ExecutionRequest) {
let error: Error;
let response: ExecutionResult<any>;
for (const executor of executors) {
try {
const executorResponse = await executor(params);
if ('errors' in executorResponse && executorResponse.errors?.length) {
response = executorResponse;
continue;
} else {
return executorResponse;
}
} catch (e) {
error = e;
}
}
if (response != null) {
return response;
}
throw error;
};
}
Example #2
Source File: index.ts From graphql-mesh with MIT License | 6 votes |
export function createJITExecutor(schema: GraphQLSchema, prefix: string, logger: Logger): Executor {
const lruCache = getLruCacheForSchema(schema);
return function jitExecutor<TReturn>(request: ExecutionRequest) {
const { document, variables, context, operationName, rootValue } = request;
const documentStr = printWithCache(document);
logger.debug(`Executing ${documentStr}`);
const cacheKey = [prefix, documentStr, operationName].join('_');
let compiledQueryFn: CompiledQuery['query'] | CompiledQuery['subscribe'] = lruCache.get(cacheKey);
if (!compiledQueryFn) {
logger.debug(`Compiling ${documentStr}`);
const compiledQuery = compileQuery(schema, document, operationName);
if (isCompiledQuery(compiledQuery)) {
const { operation } = getOperationASTFromRequest(request);
if (operation === 'subscription') {
compiledQueryFn = compiledQuery.subscribe.bind(compiledQuery);
} else {
compiledQueryFn = compiledQuery.query.bind(compiledQuery);
}
} else {
compiledQueryFn = () => compiledQuery;
}
lruCache.set(cacheKey, compiledQueryFn);
} else {
logger.debug(`Compiled version found for ${documentStr}`);
}
return compiledQueryFn(rootValue, context, variables) as ExecutionResult<TReturn>;
};
}
Example #3
Source File: index.ts From graphql-mesh with MIT License | 6 votes |
transformResult(result: ExecutionResult, delegationContext: DelegationContext) {
const errors = this.errors.get(delegationContext);
if (errors?.length) {
return {
...result,
errors: [...(result.errors || []), ...errors],
};
}
return result;
}
Example #4
Source File: index.ts From graphql-mesh with MIT License | 5 votes |
private async replaceFederationSDLWithStitchingSDL(
name: string,
oldSchema: GraphQLSchema,
executor: Executor,
stitchingDirectives: StitchingDirectivesResult
) {
const rawSourceLogger = this.logger.child(name);
rawSourceLogger.debug(`Extracting existing resolvers if available`);
const resolvers = extractResolvers(oldSchema);
let newSchema = await this.store
.proxy(`${name}_stitching`, PredefinedProxyOptions.GraphQLSchemaWithDiffing)
.getWithSet(async () => {
this.logger.debug(`Fetching Apollo Federated Service SDL for ${name}`);
const sdlQueryResult: any = (await executor({
document: parse(APOLLO_GET_SERVICE_DEFINITION_QUERY),
})) as ExecutionResult;
if (sdlQueryResult.errors?.length) {
throw new AggregateError(sdlQueryResult.errors, `Failed on fetching Federated SDL for ${name}`);
}
const federationSdl = sdlQueryResult.data._service.sdl;
this.logger.debug(`Generating Stitching SDL for ${name}`);
const stitchingSdl = federationToStitchingSDL(federationSdl, stitchingDirectives);
return buildSchema(stitchingSdl, {
assumeValid: true,
assumeValidSDL: true,
});
});
rawSourceLogger.debug(`Adding existing resolvers back to the schema`);
newSchema = addResolversToSchema({
schema: newSchema,
resolvers,
updateResolversInPlace: true,
resolverValidationOptions: {
requireResolversToMatchSchema: 'ignore',
},
});
return newSchema;
}
Example #5
Source File: graphql.d.ts From mordred with MIT License | 5 votes |
executeQuery: (
query: string,
options?: QueryOptions,
) => ExecutionResult
Example #6
Source File: tsubscribe.ts From graphql-sse with MIT License | 5 votes |
export function tsubscribe<T = unknown>(
client: Client,
payload: RequestParams,
): TSubscribe<T> {
const emitter = new EventEmitter();
const results: ExecutionResult<T, unknown>[] = [];
let error: unknown,
completed = false;
const dispose = client.subscribe<T>(payload, {
next: (value) => {
results.push(value);
emitter.emit('next');
},
error: (err) => {
error = err;
emitter.emit('err');
emitter.removeAllListeners();
},
complete: () => {
completed = true;
emitter.emit('complete');
emitter.removeAllListeners();
},
});
return {
waitForNext: (test, expire) => {
return new Promise((resolve) => {
function done() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = results.shift()!;
test?.(result);
resolve();
}
if (results.length > 0) return done();
emitter.once('next', done);
if (expire)
setTimeout(() => {
emitter.off('next', done); // expired
resolve();
}, expire);
});
},
waitForError: (test, expire) => {
return new Promise((resolve) => {
function done() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
test?.(error);
resolve();
}
if (error) return done();
emitter.once('err', done);
if (expire)
setTimeout(() => {
emitter.off('err', done); // expired
resolve();
}, expire);
});
},
waitForComplete: (test, expire) => {
return new Promise((resolve) => {
function done() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
test?.();
resolve();
}
if (completed) return done();
emitter.once('complete', done);
if (expire)
setTimeout(() => {
emitter.off('complete', done); // expired
resolve();
}, expire);
});
},
dispose,
};
}
Example #7
Source File: index.ts From graphql-mesh with MIT License | 4 votes |
async getMeshSource(): Promise<MeshSource> {
let fetch: ReturnType<typeof getCachedFetch>;
if (this.config.customFetch) {
fetch =
typeof this.config.customFetch === 'string'
? await loadFromModuleExportExpression<ReturnType<typeof getCachedFetch>>(this.config.customFetch, {
cwd: this.baseDir,
importFn: this.importFn,
defaultExportName: 'default',
})
: this.config.customFetch;
} else {
fetch = getCachedFetch(this.cache);
}
const { baseUrl: nonInterpolatedBaseUrl, operationHeaders } = this.config;
const baseUrl = stringInterpolator.parse(nonInterpolatedBaseUrl, {
env: process.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<string, string>();
const metadataJson = await this.getCachedMetadataJson(fetch);
const schemas = metadataJson.Edmx[0].DataServices[0].Schema;
const multipleSchemas = schemas.length > 1;
const namespaces = new Set<string>();
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;
}
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 = getNamedType(info.returnType) as GraphQLObjectType;
const entityTypeExtensions = actualReturnType.extensions as unknown 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 unknown 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: {
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);
if (headers.accept == null) {
headers.accept = 'application/json';
}
if (headers['content-type'] == null && method !== 'GET') {
headers['content-type'] = 'application/json';
}
return headers;
};
const { args: commonArgs, contextVariables } = parseInterpolationStrings([
...Object.values(operationHeaders || {}),
baseUrl,
]);
function getTCByTypeNames(...typeNames: string[]) {
for (const typeName of typeNames) {
try {
return schemaComposer.getAnyTC(typeName);
} catch {}
}
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) {
Object.assign(input, input.rest);
delete input.rest;
}
for (const fieldName in input) {
rebuildOpenInputObjects(input[fieldName]);
}
}
}
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(
batchResponseJson.ExceptionMessage ||
batchResponseJson.Message ||
`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(JSON.stringify(responseObj.body), {
status: responseObj.status,
headers: responseObj.headers,
});
});
}
const DATALOADER_FACTORIES = {
multipart: (context: any) =>
new DataLoader(async (requests: Request[]): Promise<Response[]> => {
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: process.env,
},
'POST'
);
batchHeaders['content-type'] = `multipart/mixed;boundary=${requestBoundary}`;
const batchResponse = await fetch(urljoin(baseUrl, '$batch'), {
method: 'POST',
body: requestBody,
headers: batchHeaders,
});
if (batchResponse.headers.get('content-type').includes('json')) {
const batchResponseJson = await batchResponse.json();
return handleBatchJsonResults(batchResponseJson, requests);
}
const batchResponseText = await batchResponse.text();
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<Response[]> => {
const batchHeaders = headersFactory(
{
context,
env: process.env,
},
'POST'
);
batchHeaders['content-type'] = 'application/json';
const batchResponse = await fetch(urljoin(baseUrl, '$batch'), {
method: 'POST',
body: JSON.stringify({
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 batchResponseJson = await batchResponse.json();
return handleBatchJsonResults(batchResponseJson, requests);
}),
none: () =>
new DataLoader(
(requests: Request[]): Promise<Response[]> => Promise.all(requests.map(request => fetch(request)))
),
};
const dataLoaderFactory = memoize1(DATALOADER_FACTORIES[this.config.batch || 'none']);
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<string, EnumTypeComposerValueConfigDefinition> = {};
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<any, unknown> = {
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<any, unknown> = {
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 unknown 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: process.env,
},
method
),
});
const response = await context[contextDataloaderName].load(request);
const responseText = await response.text();
return handleResponseText(responseText, urlString, info);
},
};
const pluralField: ObjectTypeComposerFieldConfigDefinition<any, unknown> = {
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: process.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<any, unknown> = {
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: process.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: process.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<any> = {
...commonArgs,
};
// eslint-disable-next-line prefer-const
let entitySetPath = boundFunctionObj.attributes.EntitySetPath?.split('/')[0];
let field: ObjectTypeComposerFieldConfigDefinition<any, any, any>;
let boundEntityTypeName: string;
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
// The first segment of the entity set path must match the binding parameter name
// (see: http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#_Toc38530388)
entitySetPath = (entitySetPath && entitySetPath.split('/')[0]) || parameterName;
if (entitySetPath === parameterName) {
boundEntityTypeName = getTypeNameFromRef({
typeRef: parameterTypeRef,
isInput: false,
isRequired: false,
})
.replace('[', '')
.replace(']', '');
field = {
type: returnType,
args,
resolve: async (root, args, context, info) => {
const url = new URL(root['@odata.id']);
url.href = urljoin(url.href, '/' + functionRef);
const argsEntries = Object.entries(args);
if (argsEntries.length) {
url.href += `(${argsEntries
.filter(argEntry => argEntry[0] !== 'queryOptions')
.map(([argName, value]) => [argName, typeof value === 'string' ? `'${value}'` : value])
.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: process.env,
},
method
),
});
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;
const boundEntityOtherType = getTCByTypeNames(
'I' + boundEntityTypeName,
'T' + boundEntityTypeName
) as InterfaceTypeComposer;
boundEntityType.addFields({
[functionName]: field,
});
boundEntityOtherType?.addFields({
[functionName]: field,
});
};
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: process.env,
},
method
),
body: JSON.stringify(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<any> = {
...commonArgs,
};
let entitySetPath = boundActionObj.attributes.EntitySetPath;
let boundField: ObjectTypeComposerFieldConfigDefinition<any, any, any>;
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: process.env,
},
method
),
body: JSON.stringify(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();
});
});
schemas?.forEach((schemaObj: any) => {
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: process.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<string, ObjectTypeComposerFieldConfigDefinition<any, any>> = {
[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: process.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: process.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: process.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: process.env,
},
method
),
body: JSON.stringify(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: process.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: process.env,
},
method
),
body: JSON.stringify(args.input),
});
const response = await context[contextDataloaderName].load(request);
const responseText = await response.text();
return handleResponseText(responseText, urlString, info);
},
},
});
});
});
});
// 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();
const executor = createDefaultExecutor(schema);
return {
schema,
executor: <TResult>(executionRequest: ExecutionRequest) => {
const odataContext = {
[contextDataloaderName]: dataLoaderFactory(executionRequest.context),
};
return executor({
...executionRequest,
context: {
...executionRequest.context,
...odataContext,
},
}) as ExecutionResult<TResult>;
},
contextVariables,
batch: true,
};
}
Example #8
Source File: handler.spec.ts From graphql-mesh with MIT License | 4 votes |
describe('odata', () => {
let pubsub: MeshPubSub;
let cache: KeyValueCache;
let store: MeshStore;
let logger: Logger;
beforeEach(() => {
pubsub = new PubSub();
cache = new InMemoryLRUCache();
store = new MeshStore('odata', new InMemoryStoreStorageAdapter(), {
readonly: false,
validate: false,
});
logger = new DefaultLogger('ODataTest');
resetMocks();
});
it('should create a GraphQL schema from a simple OData endpoint', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
expect(printSchema(source.schema)).toMatchSnapshot();
});
it('should create correct GraphQL schema for functions with entity set paths', async () => {
addMock('http://sample.service.com/$metadata', async () => new Response(BasicMetadata));
const handler = new ODataHandler({
name: 'SampleService',
config: {
baseUrl: 'http://sample.service.com',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
expect(printSchema(source.schema)).toMatchSnapshot();
});
it('should declare arguments for fields created from bound functions', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const personType = source.schema.getType('IPerson') as GraphQLInterfaceType;
const getFriendsTripsFunction = personType.getFields().GetFriendsTrips;
expect(getFriendsTripsFunction.args).toHaveLength(2);
const personArg = getFriendsTripsFunction.args.find(arg => arg.name === 'person');
expect(personArg).not.toBeFalsy();
expect(personArg.type.toString()).toBe('PersonInput');
const userNameArg = getFriendsTripsFunction.args.find(arg => arg.name === 'userName');
expect(userNameArg).not.toBeFalsy();
expect(userNameArg.type.toString()).toBe('String!');
});
it('should generate correct HTTP request for requesting an EntitySet', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = 'https://services.odata.org/TripPinRESTierService/People';
const correctMethod = 'GET';
let sentRequest: Request;
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(JSON.stringify({ value: [PersonMockData] }));
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
{
People {
UserName
FirstName
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
});
it('should generate correct HTTP request for requesting a single Entity by ID', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/People/SOMEID/`;
const correctMethod = 'GET';
let sentRequest: Request;
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(JSON.stringify(PersonMockData));
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
{
PeopleByUserName(UserName: "SOMEID") {
UserName
FirstName
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
});
it('should generate correct HTTP request for requesting a complex property', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/Airports/KSFO/?$select=IcaoCode,Location`;
const correctMethod = 'GET';
let sentRequest: Request;
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(
JSON.stringify({
'@odata.type': 'Microsoft.OData.Service.Sample.TrippinInMemory.Models.Airport',
IcaoCode: Date.now().toString(),
Location: {
'@odata.type': 'Microsoft.OData.Service.Sample.TrippinInMemory.Models.AirportLocation',
Loc: '',
},
})
);
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
{
AirportsByIcaoCode(IcaoCode: "KSFO") {
IcaoCode
Location {
Loc
}
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
});
it('should generate correct HTTP request for query options', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/People?$filter=FirstName eq 'Scott'`;
const correctMethod = 'GET';
let sentRequest: Request;
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(JSON.stringify({ value: [PersonMockData] }));
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
{
People(queryOptions: { filter: "FirstName eq 'Scott'" }) {
UserName
FirstName
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(decodeURIComponent(sentRequest!.url)).toBe(decodeURIComponent(correctUrl));
});
it('should generate correct HTTP request for $count', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/People/$count`;
const correctMethod = 'GET';
let sentRequest: Request;
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(JSON.stringify(20));
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
{
PeopleCount
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
});
it('should generate correct HTTP request for creating an entity', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/People`;
const correctMethod = 'POST';
const correctBody = {
UserName: 'lewisblack',
FirstName: 'Lewis',
LastName: 'Black',
Emails: ['[email protected]'],
Gender: 'Male',
FavoriteFeature: 'Feature1',
Features: ['Feature1', 'Feature2'],
AddressInfo: [
{
Address: '187 Suffolk Ln.',
City: {
Name: 'Boise',
CountryRegion: 'United States',
Region: 'ID',
},
},
],
};
let sentRequest: any;
addMock(correctUrl, async request => {
sentRequest = request.clone();
const bodyObj = await request.json();
bodyObj['@odata.type'] = 'Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person';
return new Response(JSON.stringify(bodyObj));
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
variables: {
input: correctBody,
},
document: parse(/* GraphQL */ `
mutation CreatePeople($input: PersonInput) {
createPeople(input: $input) {
UserName
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
expect(await sentRequest!.json()).toStrictEqual(correctBody);
});
it('should generate correct HTTP request for deleting an entity', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/People/SOMEID/`;
const correctMethod = 'DELETE';
let sentRequest: Request;
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(JSON.stringify({}));
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
mutation {
deletePeopleByUserName(UserName: "SOMEID")
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
});
it('should generate correct HTTP request for updating an entity', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/People/SOMEID/`;
const correctMethod = 'PATCH';
const correctBody = {
FirstName: 'Mirs',
LastName: 'King',
};
let sentRequest: Request;
addMock(correctUrl, async request => {
sentRequest = request.clone();
const returnBody = await request.json();
returnBody['@odata.type'] = 'Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person';
return new Response(JSON.stringify(returnBody));
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
variables: {
UserName: 'SOMEID',
input: correctBody,
},
document: parse(/* GraphQL */ `
mutation UpdatePeople($UserName: String!, $input: PersonUpdateInput!) {
updatePeopleByUserName(UserName: $UserName, input: $input) {
FirstName
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
expect(await sentRequest!.text()).toBe(JSON.stringify(correctBody));
});
it('should generate correct HTTP request for invoking unbound functions', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/GetNearestAirport(lat = 33, lon = -118)?$select=IcaoCode,Name`;
const correctMethod = 'GET';
let sentRequest: Request;
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(
JSON.stringify({
'@odata.type': 'Microsoft.OData.Service.Sample.TrippinInMemory.Models.Airport',
IcaoCode: Date.now().toString(),
Name: 'Name',
})
);
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
{
GetNearestAirport(lat: 33, lon: -118) {
IcaoCode
Name
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(decodeURIComponent(sentRequest!.url)).toBe(correctUrl);
});
it('should generate correct HTTP request for invoking bound functions', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/People/russellwhyte/Trips/0/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetInvolvedPeople?$select=UserName`;
const correctMethod = 'GET';
let sentRequest: Request;
addMock(`https://services.odata.org/TripPinRESTierService/People/russellwhyte/`, async () => {
return new Response(JSON.stringify(PersonMockData));
});
addMock(
`https://services.odata.org/TripPinRESTierService/People/russellwhyte/Trips?$filter=TripId eq 0&$select=TripId`,
async () => {
return new Response(JSON.stringify(TripMockData));
}
);
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(
JSON.stringify({
value: [],
})
);
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
{
PeopleByUserName(UserName: "russellwhyte") {
UserName
Trips(queryOptions: { filter: "TripId eq 0" }) {
TripId
GetInvolvedPeople {
UserName
}
}
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
});
it('should generate correct HTTP request for invoking bound functions with arguments', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/People/russellwhyte/Microsoft.OData.Service.Sample.TrippinInMemory.Models.GetFriendsTrips(userName='ronaldmundy')?$select=TripId,Name`;
const correctMethod = 'GET';
let sentRequest: Request;
addMock(`https://services.odata.org/TripPinRESTierService/People/russellwhyte/`, async () => {
return new Response(JSON.stringify(PersonMockData));
});
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(
JSON.stringify({
value: [],
})
);
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
{
PeopleByUserName(UserName: "russellwhyte") {
UserName
GetFriendsTrips(userName: "ronaldmundy") {
TripId
Name
}
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url.replace(/'/g, '%27')).toBe(correctUrl.replace(/'/g, '%27')); // apostrophe gets percent-encoded
});
it('should generate correct HTTP request for invoking unbound actions', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/ResetDataSource`;
const correctMethod = 'POST';
let sentRequest: Request;
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(JSON.stringify(true));
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
mutation {
ResetDataSource
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
});
it('should generate correct HTTP request for invoking bound actions', async () => {
addMock('https://services.odata.org/TripPinRESTierService/$metadata', async () => new Response(TripPinMetadata));
const correctUrl = `https://services.odata.org/TripPinRESTierService/People/russellwhyte/Microsoft.OData.Service.Sample.TrippinInMemory.Models.ShareTrip`;
const correctMethod = 'POST';
const correctBody = {
userName: 'scottketchum',
tripId: 0,
};
let sentRequest: Request;
addMock(`https://services.odata.org/TripPinRESTierService/People/russellwhyte/`, async () => {
return new Response(JSON.stringify(PersonMockData));
});
addMock(correctUrl, async request => {
sentRequest = request;
return new Response(JSON.stringify(true));
});
const handler = new ODataHandler({
name: 'TripPin',
config: {
baseUrl: 'https://services.odata.org/TripPinRESTierService',
customFetch: mockFetch,
},
pubsub,
cache,
store,
baseDir,
importFn,
logger,
});
const source = await handler.getMeshSource();
const graphqlResult = (await source.executor({
context: {},
document: parse(/* GraphQL */ `
mutation {
PeopleByUserName(UserName: "russellwhyte") {
ShareTrip(userName: "scottketchum", tripId: 0)
}
}
`),
})) as ExecutionResult;
expect(graphqlResult.errors).toBeFalsy();
expect(sentRequest!.method).toBe(correctMethod);
expect(sentRequest!.url).toBe(correctUrl);
expect(await sentRequest!.text()).toBe(JSON.stringify(correctBody));
});
});
Example #9
Source File: index.ts From graphql-mesh with MIT License | 4 votes |
async getUnifiedSchema({ rawSources, typeDefs, resolvers, transforms }: MeshMergerContext) {
this.logger.debug(`Creating localServiceList for gateway`);
const rawSourceMap = new Map<string, RawSourceOutput>();
const localServiceList: { name: string; typeDefs: DocumentNode }[] = [];
const sourceMap = new Map<RawSourceOutput, GraphQLSchema>();
await Promise.all(
rawSources.map(async rawSource => {
const transformedSchema = wrapSchema(rawSource as any);
rawSourceMap.set(rawSource.name, rawSource);
sourceMap.set(rawSource, transformedSchema);
const sdl = await this.store
.proxy(`${rawSource.name}_sdl`, PredefinedProxyOptions.StringWithoutValidation)
.getWithSet(async () => {
this.logger.debug(`Fetching Apollo Federated Service SDL for ${rawSource.name}`);
const sdlQueryResult: any = await execute({
schema: transformedSchema,
document: parse(SERVICE_DEFINITION_QUERY),
});
if (sdlQueryResult.errors?.length) {
throw new AggregateError(sdlQueryResult.errors, `Failed on fetching Federated SDL for ${rawSource.name}`);
}
return sdlQueryResult.data._service.sdl;
});
localServiceList.push({
name: rawSource.name,
typeDefs: parse(sdl),
});
})
);
this.logger.debug(`Creating ApolloGateway`);
const gateway = new ApolloGateway({
localServiceList,
buildService: ({ name }) => {
this.logger.debug(`Building federation service: ${name}`);
const rawSource = rawSourceMap.get(name);
const transformedSchema = sourceMap.get(rawSource);
return new LocalGraphQLDataSource(transformedSchema);
},
logger: this.logger,
debug: !!process.env.DEBUG,
serviceHealthCheck: true,
});
this.logger.debug(`Loading gateway`);
const { schema, executor: gatewayExecutor } = await gateway.load();
const schemaHash: any = printSchemaWithDirectives(schema);
let remoteSchema: GraphQLSchema = schema;
this.logger.debug(`Wrapping gateway executor in a unified schema`);
remoteSchema = wrapSchema({
schema: remoteSchema,
executor: <TReturn>({ document, info, variables, context, operationName }: ExecutionRequest) => {
const documentStr = printWithCache(document);
const { operation } = info;
// const operationName = operation.name?.value;
return gatewayExecutor({
document,
request: {
query: documentStr,
operationName,
variables,
},
operationName,
cache: this.cache,
context,
queryHash: documentStr,
logger: this.logger,
metrics: {},
source: documentStr,
operation,
schema,
schemaHash,
overallCachePolicy: {} as any,
}) as ExecutionResult<TReturn>;
},
batch: true,
});
const id$ = this.pubsub.subscribe('destroy', () => {
gateway.stop().catch(err => this.logger.error(err));
id$.then(id => this.pubsub.unsubscribe(id)).catch(err => console.error(err));
});
this.logger.debug(`Applying additionalTypeDefs`);
typeDefs?.forEach(typeDef => {
remoteSchema = extendSchema(remoteSchema, typeDef);
});
if (resolvers) {
this.logger.debug(`Applying additionalResolvers`);
for (const resolversObj of asArray(resolvers)) {
remoteSchema = addResolversToSchema({
schema: remoteSchema,
resolvers: resolversObj,
updateResolversInPlace: true,
});
}
}
if (transforms?.length) {
this.logger.debug(`Applying root level transforms`);
remoteSchema = wrapSchema({
schema: remoteSchema,
transforms: transforms as any[],
batch: true,
});
}
this.logger.debug(`Attaching sourceMap to the unified schema`);
remoteSchema.extensions = remoteSchema.extensions || {};
Object.defineProperty(remoteSchema.extensions, 'sourceMap', {
get: () => sourceMap,
});
return remoteSchema;
}