@khanacademy/wonder-blocks-data 3.0.0 → 3.1.2
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 +28 -2
- package/dist/es/index.js +204 -31
- package/dist/index.js +315 -70
- 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-data.test.js +142 -106
- package/src/hooks/__tests__/use-gql.test.js +233 -0
- package/src/hooks/use-data.js +28 -23
- package/src/hooks/use-gql.js +77 -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 +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-data
|
|
2
2
|
|
|
3
|
+
## 3.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @khanacademy/wonder-blocks-core@4.2.1
|
|
8
|
+
|
|
9
|
+
## 3.1.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 4ff59815: Add GraphQL fetch mock support to wonder-blocks-testing
|
|
14
|
+
|
|
15
|
+
## 3.1.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- b68cedfe: Add GqlRouter component
|
|
20
|
+
- c7233a97: Implement useGql hook
|
|
21
|
+
|
|
22
|
+
## 3.0.1
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- d281dac8: Ensure server-side request fulfillments can be intercepted
|
|
27
|
+
|
|
3
28
|
## 3.0.0
|
|
29
|
+
|
|
4
30
|
### Major Changes
|
|
5
31
|
|
|
6
|
-
-
|
|
7
|
-
-
|
|
32
|
+
- b252d9c8: Remove client-side caching
|
|
33
|
+
- b252d9c8: Introduce useData hook
|
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
|
/**
|
|
@@ -582,25 +583,49 @@ const useData = (handler, options) => {
|
|
|
582
583
|
// this will have cached data in those cases as it will be present on the
|
|
583
584
|
// initial render - and subsequent renders on the client it will be null.
|
|
584
585
|
const cachedResult = ResponseCache.Default.getEntry(handler, options);
|
|
585
|
-
const [result, setResult] = useState(cachedResult); //
|
|
586
|
+
const [result, setResult] = useState(cachedResult); // Lookup to see if there's an interceptor for the handler.
|
|
587
|
+
// If we have one, we need to replace the handler with one that
|
|
588
|
+
// uses the interceptor.
|
|
589
|
+
|
|
590
|
+
const interceptorMap = useContext(InterceptContext);
|
|
591
|
+
const interceptor = interceptorMap[handler.type]; // If we have an interceptor, we need to replace the handler with one that
|
|
592
|
+
// uses the interceptor. This helper function generates a new handler.
|
|
593
|
+
// We need this before we track the request as we want the interceptor
|
|
594
|
+
// to also work for tracked requests to simplify testing the server-side
|
|
595
|
+
// request fulfillment.
|
|
596
|
+
|
|
597
|
+
const getMaybeInterceptedHandler = () => {
|
|
598
|
+
if (interceptor == null) {
|
|
599
|
+
return handler;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const fulfillRequestFn = options => {
|
|
603
|
+
var _interceptor$fulfillR;
|
|
604
|
+
|
|
605
|
+
return (_interceptor$fulfillR = interceptor.fulfillRequest(options)) != null ? _interceptor$fulfillR : handler.fulfillRequest(options);
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
fulfillRequest: fulfillRequestFn,
|
|
610
|
+
getKey: options => handler.getKey(options),
|
|
611
|
+
type: handler.type,
|
|
612
|
+
hydrate: handler.hydrate
|
|
613
|
+
};
|
|
614
|
+
}; // We only track data requests when we are server-side and we don't
|
|
586
615
|
// already have a result, as given by the cachedData (which is also the
|
|
587
616
|
// initial value for the result state).
|
|
588
617
|
|
|
618
|
+
|
|
589
619
|
const maybeTrack = useContext(TrackerContext);
|
|
590
620
|
|
|
591
621
|
if (result == null && Server.isServerSide()) {
|
|
592
|
-
maybeTrack == null ? void 0 : maybeTrack(
|
|
593
|
-
} //
|
|
594
|
-
// If we have one, we need to replace the handler with one that
|
|
595
|
-
// uses the interceptor.
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const interceptorMap = useContext(InterceptContext);
|
|
599
|
-
const interceptor = interceptorMap[handler.type]; // We need to update our request when the handler changes or the key
|
|
622
|
+
maybeTrack == null ? void 0 : maybeTrack(getMaybeInterceptedHandler(), options);
|
|
623
|
+
} // We need to update our request when the handler changes or the key
|
|
600
624
|
// to the options change, so we keep track of those.
|
|
601
625
|
// However, even if we are hydrating from cache, we still need to make the
|
|
602
626
|
// request at least once, so we do not initialize these references.
|
|
603
627
|
|
|
628
|
+
|
|
604
629
|
const handlerRef = useRef();
|
|
605
630
|
const keyRef = useRef();
|
|
606
631
|
const interceptorRef = useRef(); // This effect will ensure that we fulfill the request as desired.
|
|
@@ -625,26 +650,7 @@ const useData = (handler, options) => {
|
|
|
625
650
|
if (cachedResult == null) {
|
|
626
651
|
// Mark ourselves as loading.
|
|
627
652
|
setResult(null);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const getMaybeInterceptedHandler = () => {
|
|
631
|
-
if (interceptor == null) {
|
|
632
|
-
return handler;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const fulfillRequestFn = options => {
|
|
636
|
-
var _interceptor$fulfillR;
|
|
637
|
-
|
|
638
|
-
return (_interceptor$fulfillR = interceptor.fulfillRequest(options)) != null ? _interceptor$fulfillR : handler.fulfillRequest(options);
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
return {
|
|
642
|
-
fulfillRequest: fulfillRequestFn,
|
|
643
|
-
getKey: options => handler.getKey(options),
|
|
644
|
-
type: handler.type,
|
|
645
|
-
hydrate: handler.hydrate
|
|
646
|
-
};
|
|
647
|
-
}; // We aren't server-side, so let's make the request.
|
|
653
|
+
} // We aren't server-side, so let's make the request.
|
|
648
654
|
// The request handler is in control of whether that request actually
|
|
649
655
|
// happens or not.
|
|
650
656
|
|
|
@@ -733,6 +739,173 @@ class InterceptData extends React.Component {
|
|
|
733
739
|
|
|
734
740
|
}
|
|
735
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
|
+
|
|
736
909
|
const initializeCache = source => ResponseCache.Default.initialize(source);
|
|
737
910
|
const fulfillAllDataRequests = () => {
|
|
738
911
|
if (!Server.isServerSide()) {
|
|
@@ -751,4 +924,4 @@ const hasUnfulfilledRequests = () => {
|
|
|
751
924
|
const removeFromCache = (handler, options) => ResponseCache.Default.remove(handler, options);
|
|
752
925
|
const removeAllFromCache = (handler, predicate) => ResponseCache.Default.removeAll(handler, predicate);
|
|
753
926
|
|
|
754
|
-
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 };
|