Skip to content

Commit 9e5884b

Browse files
authored
Merge pull request #159 from pgilad/refactor/fetch-async
Simplify fetch async usage and unwrap global try/catch
2 parents cac0b0f + ca2d61d commit 9e5884b

File tree

1 file changed

+175
-184
lines changed

1 file changed

+175
-184
lines changed

Diff for: src/restLink.ts

+175-184
Original file line numberDiff line numberDiff line change
@@ -876,200 +876,191 @@ const resolver: Resolver = async (
876876
endpoint,
877877
pathBuilder,
878878
} = directives.rest as RestLink.DirectiveOptions;
879+
879880
const endpointOption = getEndpointOptions(endpoints, endpoint);
880-
try {
881-
const neitherPathsProvided = path == null && pathBuilder == null;
881+
const neitherPathsProvided = path == null && pathBuilder == null;
882882

883-
if (neitherPathsProvided) {
884-
throw new Error(
885-
`One of ("path" | "pathBuilder") must be set in the @rest() directive. This request had neither, please add one`,
883+
if (neitherPathsProvided) {
884+
throw new Error(
885+
`One of ("path" | "pathBuilder") must be set in the @rest() directive. This request had neither, please add one`,
886+
);
887+
}
888+
if (!pathBuilder) {
889+
if (!path.includes(':')) {
890+
// Colons are the legacy route, and aren't uri encoded anyhow.
891+
pathBuilder = PathBuilder.replacerForPath(path);
892+
} else {
893+
console.warn(
894+
"Deprecated: '@rest(path:' contains a ':' colon, this format will be removed in future versions",
886895
);
887-
}
888-
if (!pathBuilder) {
889-
if (!path.includes(':')) {
890-
// Colons are the legacy route, and aren't uri encoded anyhow.
891-
pathBuilder = PathBuilder.replacerForPath(path);
892-
} else {
893-
console.warn(
894-
"Deprecated: '@rest(path:' contains a ':' colon, this format will be removed in future versions",
895-
);
896896

897-
pathBuilder = ({
898-
args,
899-
exportVariables,
900-
}: RestLink.PathBuilderProps): string => {
901-
const legacyArgs = {
902-
...args,
903-
...exportVariables,
904-
};
905-
const pathWithParams = Object.keys(legacyArgs).reduce(
906-
(acc, e) => replaceLegacyParam(acc, e, legacyArgs[e]),
907-
path,
908-
);
909-
if (pathWithParams.includes(':')) {
910-
throw new Error(
911-
'Missing parameters to run query, specify it in the query params or use ' +
912-
'an export directive. (If you need to use ":" inside a variable string' +
913-
' make sure to encode the variables properly using `encodeURIComponent' +
914-
'`. Alternatively see documentation about using pathBuilder.)',
915-
);
916-
}
917-
return pathWithParams;
897+
pathBuilder = ({
898+
args,
899+
exportVariables,
900+
}: RestLink.PathBuilderProps): string => {
901+
const legacyArgs = {
902+
...args,
903+
...exportVariables,
918904
};
919-
}
920-
}
921-
const allParams: RestLink.PathBuilderProps = {
922-
args,
923-
exportVariables,
924-
context,
925-
'@rest': directives.rest,
926-
replacer: pathBuilder,
927-
};
928-
const pathWithParams = pathBuilder(allParams);
929-
930-
let {
931-
method,
932-
type,
933-
bodyBuilder,
934-
bodyKey,
935-
fieldNameDenormalizer: perRequestNameDenormalizer,
936-
bodySerializer,
937-
} = directives.rest as RestLink.DirectiveOptions;
938-
if (!method) {
939-
method = 'GET';
940-
}
941-
942-
let body = undefined;
943-
let overrideHeaders: Headers = undefined;
944-
if (
945-
-1 === ['GET', 'DELETE'].indexOf(method) &&
946-
operationType === 'mutation'
947-
) {
948-
// Prepare our body!
949-
if (!bodyBuilder) {
950-
// By convention GraphQL recommends mutations having a single argument named "input"
951-
// https://dev-blog.apollodata.com/designing-graphql-mutations-e09de826ed97
952-
953-
const maybeBody =
954-
allParams.exportVariables[bodyKey || 'input'] ||
955-
allParams.args[bodyKey || 'input'];
956-
if (!maybeBody) {
905+
const pathWithParams = Object.keys(legacyArgs).reduce(
906+
(acc, e) => replaceLegacyParam(acc, e, legacyArgs[e]),
907+
path,
908+
);
909+
if (pathWithParams.includes(':')) {
957910
throw new Error(
958-
'[GraphQL mutation using a REST call without a body]. No `input` was detected. Pass bodyKey, or bodyBuilder to the @rest() directive to resolve this.',
911+
'Missing parameters to run query, specify it in the query params or use ' +
912+
'an export directive. (If you need to use ":" inside a variable string' +
913+
' make sure to encode the variables properly using `encodeURIComponent' +
914+
'`. Alternatively see documentation about using pathBuilder.)',
959915
);
960916
}
917+
return pathWithParams;
918+
};
919+
}
920+
}
921+
const allParams: RestLink.PathBuilderProps = {
922+
args,
923+
exportVariables,
924+
context,
925+
'@rest': directives.rest,
926+
replacer: pathBuilder,
927+
};
928+
const pathWithParams = pathBuilder(allParams);
961929

962-
bodyBuilder = (argsWithExport: object) => {
963-
return maybeBody;
964-
};
965-
}
966-
body = convertObjectKeys(
967-
bodyBuilder(allParams),
968-
perRequestNameDenormalizer ||
969-
linkLevelNameDenormalizer ||
970-
noOpNameNormalizer,
971-
);
972-
973-
let serializedBody: RestLink.SerializedBody;
930+
let {
931+
method,
932+
type,
933+
bodyBuilder,
934+
bodyKey,
935+
fieldNameDenormalizer: perRequestNameDenormalizer,
936+
bodySerializer,
937+
} = directives.rest as RestLink.DirectiveOptions;
938+
if (!method) {
939+
method = 'GET';
940+
}
974941

975-
if (typeof bodySerializer === 'string') {
976-
if (!serializers.hasOwnProperty(bodySerializer)) {
977-
throw new Error(
978-
'"bodySerializer" must correspond to configured serializer. ' +
979-
`Please make sure to specify a serializer called ${bodySerializer} in the "bodySerializers" property of the RestLink.`,
980-
);
981-
}
982-
serializedBody = serializers[bodySerializer](body, headers);
983-
} else {
984-
serializedBody = bodySerializer
985-
? bodySerializer(body, headers)
986-
: serializers[DEFAULT_SERIALIZER_KEY](body, headers);
942+
let body = undefined;
943+
let overrideHeaders: Headers = undefined;
944+
if (
945+
-1 === ['GET', 'DELETE'].indexOf(method) &&
946+
operationType === 'mutation'
947+
) {
948+
// Prepare our body!
949+
if (!bodyBuilder) {
950+
// By convention GraphQL recommends mutations having a single argument named "input"
951+
// https://dev-blog.apollodata.com/designing-graphql-mutations-e09de826ed97
952+
953+
const maybeBody =
954+
allParams.exportVariables[bodyKey || 'input'] ||
955+
allParams.args[bodyKey || 'input'];
956+
if (!maybeBody) {
957+
throw new Error(
958+
'[GraphQL mutation using a REST call without a body]. No `input` was detected. Pass bodyKey, or bodyBuilder to the @rest() directive to resolve this.',
959+
);
987960
}
988961

989-
body = serializedBody.body;
990-
overrideHeaders = new Headers(serializedBody.headers);
962+
bodyBuilder = (argsWithExport: object) => {
963+
return maybeBody;
964+
};
991965
}
966+
body = convertObjectKeys(
967+
bodyBuilder(allParams),
968+
perRequestNameDenormalizer ||
969+
linkLevelNameDenormalizer ||
970+
noOpNameNormalizer,
971+
);
992972

993-
validateRequestMethodForOperationType(method, operationType || 'query');
994-
return await (customFetch || fetch)(
995-
`${endpointOption.uri}${pathWithParams}`,
996-
{
997-
method,
998-
headers: overrideHeaders || headers,
999-
body: body,
1000-
1001-
// Only set credentials if they're non-null as some browsers throw an exception:
1002-
// https://github.com/apollographql/apollo-link-rest/issues/121#issuecomment-396049677
1003-
...(credentials ? { credentials } : {}),
1004-
},
1005-
)
1006-
.then(async res => {
1007-
context.responses.push(res);
1008-
1009-
// All other success responses
1010-
if (res.status < 300) {
1011-
// HTTP-204 means "no-content", similarly Content-Length implies the same
1012-
// This commonly occurs when you POST/PUT to the server, and it acknowledges
1013-
// success, but doesn't return your Resource.
1014-
if (res.status === 204 || res.headers.get('Content-Length') === '0') {
1015-
return Promise.resolve({});
1016-
}
1017-
1018-
return res.json();
1019-
}
973+
let serializedBody: RestLink.SerializedBody;
1020974

1021-
// In a GraphQL context a missing resource should be indicated by
1022-
// a null value rather than throwing a network error
1023-
if (res.status === 404) {
1024-
return Promise.resolve(null);
1025-
}
1026-
// Default error handling:
1027-
// Throw a JSError, that will be available under the
1028-
// "Network error" category in apollo-link-error
1029-
let parsed: any;
1030-
// responses need to be cloned as they can only be read once
1031-
try {
1032-
parsed = await res.clone().json();
1033-
} catch (error) {
1034-
// its not json
1035-
parsed = await res.clone().text();
1036-
}
1037-
rethrowServerSideError(
1038-
res,
1039-
parsed,
1040-
`Response not successful: Received status code ${res.status}`,
975+
if (typeof bodySerializer === 'string') {
976+
if (!serializers.hasOwnProperty(bodySerializer)) {
977+
throw new Error(
978+
'"bodySerializer" must correspond to configured serializer. ' +
979+
`Please make sure to specify a serializer called ${bodySerializer} in the "bodySerializers" property of the RestLink.`,
1041980
);
1042-
})
1043-
.then(result => {
1044-
if (endpointOption.responseTransformer) {
1045-
return endpointOption.responseTransformer(result, type);
1046-
}
981+
}
982+
serializedBody = serializers[bodySerializer](body, headers);
983+
} else {
984+
serializedBody = bodySerializer
985+
? bodySerializer(body, headers)
986+
: serializers[DEFAULT_SERIALIZER_KEY](body, headers);
987+
}
1047988

1048-
if (responseTransformer) {
1049-
return responseTransformer(result, type);
1050-
}
989+
body = serializedBody.body;
990+
overrideHeaders = new Headers(serializedBody.headers);
991+
}
1051992

1052-
return result;
1053-
})
1054-
.then(
1055-
result =>
1056-
fieldNameNormalizer == null
1057-
? result
1058-
: convertObjectKeys(result, fieldNameNormalizer),
1059-
)
1060-
.then(result =>
1061-
findRestDirectivesThenInsertNullsForOmittedFields(
1062-
resultKey,
1063-
result,
1064-
mainDefinition,
1065-
fragmentMap,
1066-
mainDefinition.selectionSet,
1067-
),
1068-
)
1069-
.then(result => addTypeNameToResult(result, type, typePatcher));
1070-
} catch (error) {
1071-
throw error;
993+
validateRequestMethodForOperationType(method, operationType || 'query');
994+
995+
const requestParams = {
996+
method,
997+
headers: overrideHeaders || headers,
998+
body: body,
999+
1000+
// Only set credentials if they're non-null as some browsers throw an exception:
1001+
// https://github.com/apollographql/apollo-link-rest/issues/121#issuecomment-396049677
1002+
...(credentials ? { credentials } : {}),
1003+
};
1004+
const requestUrl = `${endpointOption.uri}${pathWithParams}`;
1005+
1006+
const response = await (customFetch || fetch)(requestUrl, requestParams);
1007+
context.responses.push(response);
1008+
1009+
let result;
1010+
if (response.ok) {
1011+
if (
1012+
response.status === 204 ||
1013+
response.headers.get('Content-Length') === '0'
1014+
) {
1015+
// HTTP-204 means "no-content", similarly Content-Length implies the same
1016+
// This commonly occurs when you POST/PUT to the server, and it acknowledges
1017+
// success, but doesn't return your Resource.
1018+
result = {};
1019+
} else {
1020+
result = await response.json();
1021+
}
1022+
} else if (response.status === 404) {
1023+
// In a GraphQL context a missing resource should be indicated by
1024+
// a null value rather than throwing a network error
1025+
result = null;
1026+
} else {
1027+
// Default error handling:
1028+
// Throw a JSError, that will be available under the
1029+
// "Network error" category in apollo-link-error
1030+
let parsed: any;
1031+
// responses need to be cloned as they can only be read once
1032+
try {
1033+
parsed = await response.clone().json();
1034+
} catch (error) {
1035+
// its not json
1036+
parsed = await response.clone().text();
1037+
}
1038+
rethrowServerSideError(
1039+
response,
1040+
parsed,
1041+
`Response not successful: Received status code ${response.status}`,
1042+
);
1043+
}
1044+
1045+
if (endpointOption.responseTransformer) {
1046+
result = endpointOption.responseTransformer(result, type);
1047+
} else if (responseTransformer) {
1048+
result = responseTransformer(result, type);
1049+
}
1050+
1051+
if (fieldNameNormalizer !== null) {
1052+
result = convertObjectKeys(result, fieldNameNormalizer);
10721053
}
1054+
1055+
result = findRestDirectivesThenInsertNullsForOmittedFields(
1056+
resultKey,
1057+
result,
1058+
mainDefinition,
1059+
fragmentMap,
1060+
mainDefinition.selectionSet,
1061+
);
1062+
1063+
return addTypeNameToResult(result, type, typePatcher);
10731064
};
10741065

10751066
/**
@@ -1097,15 +1088,15 @@ const DEFAULT_JSON_SERIALIZER: RestLink.Serializer = (
10971088
* RestLink is an apollo-link for communicating with REST services using GraphQL on the client-side
10981089
*/
10991090
export class RestLink extends ApolloLink {
1100-
private endpoints: RestLink.Endpoints;
1101-
private headers: Headers;
1102-
private fieldNameNormalizer: RestLink.FieldNameNormalizer;
1103-
private fieldNameDenormalizer: RestLink.FieldNameNormalizer;
1104-
private typePatcher: RestLink.FunctionalTypePatcher;
1105-
private credentials: RequestCredentials;
1106-
private customFetch: RestLink.CustomFetch;
1107-
private serializers: RestLink.Serializers;
1108-
private responseTransformer: RestLink.ResponseTransformer;
1091+
private readonly endpoints: RestLink.Endpoints;
1092+
private readonly headers: Headers;
1093+
private readonly fieldNameNormalizer: RestLink.FieldNameNormalizer;
1094+
private readonly fieldNameDenormalizer: RestLink.FieldNameNormalizer;
1095+
private readonly typePatcher: RestLink.FunctionalTypePatcher;
1096+
private readonly credentials: RequestCredentials;
1097+
private readonly customFetch: RestLink.CustomFetch;
1098+
private readonly serializers: RestLink.Serializers;
1099+
private readonly responseTransformer: RestLink.ResponseTransformer;
11091100

11101101
constructor({
11111102
uri,

0 commit comments

Comments
 (0)