@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 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 };