@khanacademy/wonder-blocks-data 3.0.1 → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/es/index.js +170 -2
- package/dist/index.js +281 -41
- package/package.json +4 -3
- package/src/components/__tests__/gql-router.test.js +64 -0
- package/src/components/gql-router.js +66 -0
- package/src/hooks/__tests__/use-gql.test.js +233 -0
- package/src/hooks/use-gql.js +72 -0
- package/src/index.js +12 -6
- package/src/util/__tests__/get-gql-data-from-response.test.js +187 -0
- package/src/util/get-gql-data-from-response.js +69 -0
- package/src/util/gql-error.js +36 -0
- package/src/util/gql-router-context.js +6 -0
- package/src/util/gql-types.js +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-data
|
|
2
2
|
|
|
3
|
+
## 3.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9931ae6b: Simplify GQL types
|
|
8
|
+
|
|
9
|
+
## 3.1.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- @khanacademy/wonder-blocks-core@4.2.1
|
|
14
|
+
|
|
15
|
+
## 3.1.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 4ff59815: Add GraphQL fetch mock support to wonder-blocks-testing
|
|
20
|
+
|
|
21
|
+
## 3.1.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- b68cedfe: Add GqlRouter component
|
|
26
|
+
- c7233a97: Implement useGql hook
|
|
27
|
+
|
|
3
28
|
## 3.0.1
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/dist/es/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Server } from '@khanacademy/wonder-blocks-core';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { useState, useContext, useRef, useEffect } from 'react';
|
|
3
|
+
import { useState, useContext, useRef, useEffect, useMemo } from 'react';
|
|
4
4
|
import _extends from '@babel/runtime/helpers/extends';
|
|
5
|
+
import { Errors, KindError } from '@khanacademy/wonder-stuff-core';
|
|
5
6
|
|
|
6
7
|
function deepClone(source) {
|
|
7
8
|
/**
|
|
@@ -738,6 +739,173 @@ class InterceptData extends React.Component {
|
|
|
738
739
|
|
|
739
740
|
}
|
|
740
741
|
|
|
742
|
+
const GqlRouterContext = /*#__PURE__*/React.createContext(null);
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Configure GraphQL routing for GraphQL hooks and components.
|
|
746
|
+
*
|
|
747
|
+
* These can be nested. Components and hooks relying on the GraphQL routing
|
|
748
|
+
* will use the configuration from their closest ancestral GqlRouter.
|
|
749
|
+
*/
|
|
750
|
+
const GqlRouter = ({
|
|
751
|
+
defaultContext: thisDefaultContext,
|
|
752
|
+
fetch: thisFetch,
|
|
753
|
+
children
|
|
754
|
+
}) => {
|
|
755
|
+
// We don't care if we're nested. We always force our callers to define
|
|
756
|
+
// everything. It makes for a clearer API and requires less error checking
|
|
757
|
+
// code (assuming our flow types are correct). We also don't default fetch
|
|
758
|
+
// to anything - our callers can tell us what function to use quite easily.
|
|
759
|
+
// If code that consumes this wants more nuanced nesting, it can implement
|
|
760
|
+
// it within its own GqlRouter than then defers to this one.
|
|
761
|
+
// We want to always use the same object if things haven't changed to avoid
|
|
762
|
+
// over-rendering consumers of our context, let's memoize the configuration.
|
|
763
|
+
// By doing this, if a component under children that uses this context
|
|
764
|
+
// uses React.memo, we won't force it to re-render every time we render
|
|
765
|
+
// because we'll only change the context value if something has actually
|
|
766
|
+
// changed.
|
|
767
|
+
const configuration = React.useMemo(() => ({
|
|
768
|
+
fetch: thisFetch,
|
|
769
|
+
defaultContext: thisDefaultContext
|
|
770
|
+
}), [thisDefaultContext, thisFetch]);
|
|
771
|
+
return /*#__PURE__*/React.createElement(GqlRouterContext.Provider, {
|
|
772
|
+
value: configuration
|
|
773
|
+
}, children);
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Error kinds for GqlError.
|
|
778
|
+
*/
|
|
779
|
+
const GqlErrors = Object.freeze(_extends({}, Errors, {
|
|
780
|
+
Network: "Network",
|
|
781
|
+
Parse: "Parse",
|
|
782
|
+
BadResponse: "BadResponse",
|
|
783
|
+
ErrorResult: "ErrorResult"
|
|
784
|
+
}));
|
|
785
|
+
/**
|
|
786
|
+
* An error from the GQL API.
|
|
787
|
+
*/
|
|
788
|
+
|
|
789
|
+
class GqlError extends KindError {
|
|
790
|
+
constructor(message, kind, {
|
|
791
|
+
metadata,
|
|
792
|
+
cause
|
|
793
|
+
} = {}) {
|
|
794
|
+
super(message, kind, {
|
|
795
|
+
metadata,
|
|
796
|
+
cause,
|
|
797
|
+
prefix: "Gql"
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Validate a GQL operation response and extract the data.
|
|
805
|
+
*/
|
|
806
|
+
|
|
807
|
+
const getGqlDataFromResponse = async response => {
|
|
808
|
+
// Get the response as text, that way we can use the text in error
|
|
809
|
+
// messaging, should our parsing fail.
|
|
810
|
+
const bodyText = await response.text();
|
|
811
|
+
let result;
|
|
812
|
+
|
|
813
|
+
try {
|
|
814
|
+
result = JSON.parse(bodyText);
|
|
815
|
+
} catch (e) {
|
|
816
|
+
throw new GqlError("Failed to parse response", GqlErrors.Parse, {
|
|
817
|
+
metadata: {
|
|
818
|
+
statusCode: response.status,
|
|
819
|
+
bodyText
|
|
820
|
+
},
|
|
821
|
+
cause: e
|
|
822
|
+
});
|
|
823
|
+
} // Check for a bad status code.
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
if (response.status >= 300) {
|
|
827
|
+
throw new GqlError("Response unsuccessful", GqlErrors.Network, {
|
|
828
|
+
metadata: {
|
|
829
|
+
statusCode: response.status,
|
|
830
|
+
result
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
} // Check that we have a valid result payload.
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
if ( // Flow shouldn't be warning about this.
|
|
837
|
+
// $FlowIgnore[method-unbinding]
|
|
838
|
+
!Object.prototype.hasOwnProperty.call(result, "data") && // Flow shouldn't be warning about this.
|
|
839
|
+
// $FlowIgnore[method-unbinding]
|
|
840
|
+
!Object.prototype.hasOwnProperty.call(result, "errors")) {
|
|
841
|
+
throw new GqlError("Server response missing", GqlErrors.BadResponse, {
|
|
842
|
+
metadata: {
|
|
843
|
+
statusCode: response.status,
|
|
844
|
+
result
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
} // If the response payload has errors, throw an error.
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
if (result.errors != null && Array.isArray(result.errors) && result.errors.length > 0) {
|
|
851
|
+
throw new GqlError("GraphQL errors", GqlErrors.ErrorResult, {
|
|
852
|
+
metadata: {
|
|
853
|
+
statusCode: response.status,
|
|
854
|
+
result
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
} // We got here, so return the data.
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
return result.data;
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Hook to obtain a gqlFetch function for performing GraphQL requests.
|
|
865
|
+
*
|
|
866
|
+
* The fetch function will resolve null if the request was aborted, otherwise
|
|
867
|
+
* it will resolve the data returned by the GraphQL server.
|
|
868
|
+
*/
|
|
869
|
+
const useGql = () => {
|
|
870
|
+
// This hook only works if the `GqlRouter` has been used to setup context.
|
|
871
|
+
const gqlRouterContext = useContext(GqlRouterContext);
|
|
872
|
+
|
|
873
|
+
if (gqlRouterContext == null) {
|
|
874
|
+
throw new GqlError("No GqlRouter", GqlErrors.Internal);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const {
|
|
878
|
+
fetch,
|
|
879
|
+
defaultContext
|
|
880
|
+
} = gqlRouterContext; // Let's memoize the gqlFetch function we create based off our context.
|
|
881
|
+
// That way, even if the context happens to change, if its values don't
|
|
882
|
+
// we give the same function instance back to our callers instead of
|
|
883
|
+
// making a new one. That then means they can safely use the return value
|
|
884
|
+
// in hooks deps without fear of it triggering extra renders.
|
|
885
|
+
|
|
886
|
+
const gqlFetch = useMemo(() => (operation, options = Object.freeze({})) => {
|
|
887
|
+
const {
|
|
888
|
+
variables,
|
|
889
|
+
context
|
|
890
|
+
} = options; // Invoke the fetch and extract the data.
|
|
891
|
+
|
|
892
|
+
return fetch(operation, variables, _extends({}, defaultContext, context)).then(getGqlDataFromResponse, error => {
|
|
893
|
+
// Return null if the request was aborted.
|
|
894
|
+
// The only way to detect this reliably, it seems, is to
|
|
895
|
+
// check the error name and see if it's "AbortError" (this
|
|
896
|
+
// is also what Apollo does).
|
|
897
|
+
// Even then, it's reliant on the fetch supporting aborts.
|
|
898
|
+
if (error.name === "AbortError") {
|
|
899
|
+
return null;
|
|
900
|
+
} // Need to make sure we pass other errors along.
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
throw error;
|
|
904
|
+
});
|
|
905
|
+
}, [fetch, defaultContext]);
|
|
906
|
+
return gqlFetch;
|
|
907
|
+
};
|
|
908
|
+
|
|
741
909
|
const initializeCache = source => ResponseCache.Default.initialize(source);
|
|
742
910
|
const fulfillAllDataRequests = () => {
|
|
743
911
|
if (!Server.isServerSide()) {
|
|
@@ -756,4 +924,4 @@ const hasUnfulfilledRequests = () => {
|
|
|
756
924
|
const removeFromCache = (handler, options) => ResponseCache.Default.remove(handler, options);
|
|
757
925
|
const removeAllFromCache = (handler, predicate) => ResponseCache.Default.removeAll(handler, predicate);
|
|
758
926
|
|
|
759
|
-
export { Data, InterceptData, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache, useData };
|
|
927
|
+
export { Data, GqlError, GqlErrors, GqlRouter, InterceptData, RequestHandler, TrackData, fulfillAllDataRequests, hasUnfulfilledRequests, initializeCache, removeAllFromCache, removeFromCache, useData, useGql };
|