@khanacademy/wonder-blocks-data 15.0.3 → 15.1.0

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,22 @@
1
1
  # @khanacademy/wonder-blocks-data
2
2
 
3
+ ## 15.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6c55a12: Add `ErrorResultGqlError`, a typed `GqlError` thrown when a GraphQL response reports errors, exposing `statusCode` and the parsed `result` payload (partial `data` plus `errors`) so consumers can use partial results without type suppression. Also adds the `isErrorResultGqlError` type guard and the `GqlPartialData`, `GqlResponseError`, and `GqlErrorResultPayload` types.
8
+
9
+ ### Patch Changes
10
+
11
+ - @khanacademy/wonder-blocks-core@12.5.0
12
+
13
+ ## 15.0.4
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [63dcc94]
18
+ - @khanacademy/wonder-blocks-core@12.5.0
19
+
3
20
  ## 15.0.3
4
21
 
5
22
  ### Patch Changes
package/dist/es/index.js CHANGED
@@ -50,7 +50,9 @@ const toString=value=>{if(typeof value==="string"){return value}if(typeof value=
50
50
 
51
51
  const GqlErrors=Object.freeze({Internal:"Internal",BadResponse:"BadResponse",ErrorResult:"ErrorResult"});class GqlError extends KindError{constructor(message,kind,{metadata,cause}={}){super(message,kind,{metadata,cause,name:"Gql"});}}
52
52
 
53
- const getGqlDataFromResponse=async response=>{const bodyText=await response.text();let result;try{result=JSON.parse(bodyText);}catch(e){throw new DataError("Failed to parse response",DataErrors.Parse,{metadata:{statusCode:response.status,bodyText},cause:e})}if(response.status>=300){throw new DataError("Response unsuccessful",DataErrors.Network,{metadata:{statusCode:response.status,result}})}if(!Object.prototype.hasOwnProperty.call(result,"data")&&!Object.prototype.hasOwnProperty.call(result,"errors")){throw new GqlError("Server response missing",GqlErrors.BadResponse,{metadata:{statusCode:response.status,result}})}if(result.errors!=null&&Array.isArray(result.errors)&&result.errors.length>0){throw new GqlError("GraphQL errors",GqlErrors.ErrorResult,{metadata:{statusCode:response.status,result}})}return result.data};
53
+ class ErrorResultGqlError extends GqlError{constructor(statusCode,result){super("GraphQL errors",GqlErrors.ErrorResult,{metadata:{statusCode,result:result}});this.statusCode=statusCode;this.result=result;}}
54
+
55
+ const getGqlDataFromResponse=async response=>{const bodyText=await response.text();let result;try{result=JSON.parse(bodyText);}catch(e){throw new DataError("Failed to parse response",DataErrors.Parse,{metadata:{statusCode:response.status,bodyText},cause:e})}if(response.status>=300){throw new DataError("Response unsuccessful",DataErrors.Network,{metadata:{statusCode:response.status,result}})}if(!Object.prototype.hasOwnProperty.call(result,"data")&&!Object.prototype.hasOwnProperty.call(result,"errors")){throw new GqlError("Server response missing",GqlErrors.BadResponse,{metadata:{statusCode:response.status,result}})}if(result.errors!=null&&Array.isArray(result.errors)&&result.errors.length>0){throw new ErrorResultGqlError(response.status,result)}return result.data};
54
56
 
55
57
  const DocumentTypes=Object.freeze({query:"query",mutation:"mutation"});const cache=new Map;function graphQLDocumentNodeParser(document){const cached=cache.get(document);if(cached){return cached}if(!document?.kind){if(process.env.NODE_ENV==="production"){throw new DataError("Bad DocumentNode",DataErrors.InvalidInput)}else {throw new DataError(`Argument of ${JSON.stringify(document)} passed to parser was not a valid GraphQL `+`DocumentNode. You may need to use 'graphql-tag' or another method `+`to convert your operation into a document`,DataErrors.InvalidInput)}}const fragments=document.definitions.filter(x=>x.kind==="FragmentDefinition");const queries=document.definitions.filter(x=>x.kind==="OperationDefinition"&&x.operation==="query");const mutations=document.definitions.filter(x=>x.kind==="OperationDefinition"&&x.operation==="mutation");const subscriptions=document.definitions.filter(x=>x.kind==="OperationDefinition"&&x.operation==="subscription");if(fragments.length&&!queries.length&&!mutations.length){if(process.env.NODE_ENV==="production"){throw new DataError("Fragment only",DataErrors.InvalidInput)}else {throw new DataError(`Passing only a fragment to 'graphql' is not supported. `+`You must include a query or mutation as well`,DataErrors.InvalidInput)}}if(subscriptions.length){if(process.env.NODE_ENV==="production"){throw new DataError("No subscriptions",DataErrors.InvalidInput)}else {throw new DataError(`We do not support subscriptions. `+`${JSON.stringify(document)} had ${subscriptions.length} subscriptions`,DataErrors.InvalidInput)}}if(queries.length+mutations.length>1){if(process.env.NODE_ENV==="production"){throw new DataError("Too many ops",DataErrors.InvalidInput)}else {throw new DataError(`We only support one query or mutation per component. `+`${JSON.stringify(document)} had ${queries.length} queries and `+`${mutations.length} mutations. `,DataErrors.InvalidInput)}}const type=queries.length?DocumentTypes.query:DocumentTypes.mutation;const definitions=queries.length?queries:mutations;const definition=definitions[0];const variables=definition.variableDefinitions||[];const name=definition.name?.kind==="Name"?definition.name.value:"data";const payload={name,type,variables};cache.set(document,payload);return payload}
56
58
 
@@ -66,4 +68,6 @@ const useGqlRouterContext=(contextOverrides={})=>{const gqlRouterContext=useCont
66
68
 
67
69
  const useGql=(context={})=>{const gqlRouterContext=useGqlRouterContext(context);const gqlFetch=useCallback((operation,options=Object.freeze({}))=>{const{fetch,defaultContext}=gqlRouterContext;const{variables,context={}}=options;const finalContext=mergeGqlContext(defaultContext,context);return fetch(operation,variables,finalContext).then(response=>getGqlDataFromResponse(response))},[gqlRouterContext]);return gqlFetch};
68
70
 
69
- export { Data, DataError, DataErrors, FetchPolicy, GqlError, GqlErrors, GqlRouter, InterceptRequests, ScopedInMemoryCache, SerializableInMemoryCache, SharedCache, Status, TrackData, WhenClientSide, abortInflightRequests, fetchTrackedRequests, getGqlDataFromResponse, getGqlRequestId, graphQLDocumentNodeParser, hasTrackedRequestsToBeFetched, initializeHydrationCache, purgeCaches, purgeHydrationCache, toGqlOperation, useCachedEffect, useGql, useHydratableEffect, useServerEffect, useSharedCache };
71
+ const isErrorResultGqlError=error=>error instanceof ErrorResultGqlError;
72
+
73
+ export { Data, DataError, DataErrors, ErrorResultGqlError, FetchPolicy, GqlError, GqlErrors, GqlRouter, InterceptRequests, ScopedInMemoryCache, SerializableInMemoryCache, SharedCache, Status, TrackData, WhenClientSide, abortInflightRequests, fetchTrackedRequests, getGqlDataFromResponse, getGqlRequestId, graphQLDocumentNodeParser, hasTrackedRequestsToBeFetched, initializeHydrationCache, isErrorResultGqlError, purgeCaches, purgeHydrationCache, toGqlOperation, useCachedEffect, useGql, useHydratableEffect, useServerEffect, useSharedCache };
package/dist/index.d.ts CHANGED
@@ -21,4 +21,6 @@ export { toGqlOperation } from "./util/to-gql-operation";
21
21
  export { GqlRouter } from "./components/gql-router";
22
22
  export { useGql } from "./hooks/use-gql";
23
23
  export { GqlError, GqlErrors } from "./util/gql-error";
24
- export type { GqlContext, GqlOperation, GqlOperationType, GqlFetchOptions, GqlFetchFn, } from "./util/gql-types";
24
+ export { ErrorResultGqlError } from "./util/error-result-gql-error";
25
+ export { isErrorResultGqlError } from "./util/is-error-result-gql-error";
26
+ export type { GqlContext, GqlOperation, GqlOperationType, GqlFetchOptions, GqlFetchFn, GqlPartialData, GqlResponseError, GqlErrorResultPayload, } from "./util/gql-types";
package/dist/index.js CHANGED
@@ -73,7 +73,9 @@ const toString=value=>{if(typeof value==="string"){return value}if(typeof value=
73
73
 
74
74
  const GqlErrors=Object.freeze({Internal:"Internal",BadResponse:"BadResponse",ErrorResult:"ErrorResult"});class GqlError extends wonderStuffCore.KindError{constructor(message,kind,{metadata,cause}={}){super(message,kind,{metadata,cause,name:"Gql"});}}
75
75
 
76
- const getGqlDataFromResponse=async response=>{const bodyText=await response.text();let result;try{result=JSON.parse(bodyText);}catch(e){throw new DataError("Failed to parse response",DataErrors.Parse,{metadata:{statusCode:response.status,bodyText},cause:e})}if(response.status>=300){throw new DataError("Response unsuccessful",DataErrors.Network,{metadata:{statusCode:response.status,result}})}if(!Object.prototype.hasOwnProperty.call(result,"data")&&!Object.prototype.hasOwnProperty.call(result,"errors")){throw new GqlError("Server response missing",GqlErrors.BadResponse,{metadata:{statusCode:response.status,result}})}if(result.errors!=null&&Array.isArray(result.errors)&&result.errors.length>0){throw new GqlError("GraphQL errors",GqlErrors.ErrorResult,{metadata:{statusCode:response.status,result}})}return result.data};
76
+ class ErrorResultGqlError extends GqlError{constructor(statusCode,result){super("GraphQL errors",GqlErrors.ErrorResult,{metadata:{statusCode,result:result}});this.statusCode=statusCode;this.result=result;}}
77
+
78
+ const getGqlDataFromResponse=async response=>{const bodyText=await response.text();let result;try{result=JSON.parse(bodyText);}catch(e){throw new DataError("Failed to parse response",DataErrors.Parse,{metadata:{statusCode:response.status,bodyText},cause:e})}if(response.status>=300){throw new DataError("Response unsuccessful",DataErrors.Network,{metadata:{statusCode:response.status,result}})}if(!Object.prototype.hasOwnProperty.call(result,"data")&&!Object.prototype.hasOwnProperty.call(result,"errors")){throw new GqlError("Server response missing",GqlErrors.BadResponse,{metadata:{statusCode:response.status,result}})}if(result.errors!=null&&Array.isArray(result.errors)&&result.errors.length>0){throw new ErrorResultGqlError(response.status,result)}return result.data};
77
79
 
78
80
  const DocumentTypes=Object.freeze({query:"query",mutation:"mutation"});const cache=new Map;function graphQLDocumentNodeParser(document){const cached=cache.get(document);if(cached){return cached}if(!document?.kind){if(process.env.NODE_ENV==="production"){throw new DataError("Bad DocumentNode",DataErrors.InvalidInput)}else {throw new DataError(`Argument of ${JSON.stringify(document)} passed to parser was not a valid GraphQL `+`DocumentNode. You may need to use 'graphql-tag' or another method `+`to convert your operation into a document`,DataErrors.InvalidInput)}}const fragments=document.definitions.filter(x=>x.kind==="FragmentDefinition");const queries=document.definitions.filter(x=>x.kind==="OperationDefinition"&&x.operation==="query");const mutations=document.definitions.filter(x=>x.kind==="OperationDefinition"&&x.operation==="mutation");const subscriptions=document.definitions.filter(x=>x.kind==="OperationDefinition"&&x.operation==="subscription");if(fragments.length&&!queries.length&&!mutations.length){if(process.env.NODE_ENV==="production"){throw new DataError("Fragment only",DataErrors.InvalidInput)}else {throw new DataError(`Passing only a fragment to 'graphql' is not supported. `+`You must include a query or mutation as well`,DataErrors.InvalidInput)}}if(subscriptions.length){if(process.env.NODE_ENV==="production"){throw new DataError("No subscriptions",DataErrors.InvalidInput)}else {throw new DataError(`We do not support subscriptions. `+`${JSON.stringify(document)} had ${subscriptions.length} subscriptions`,DataErrors.InvalidInput)}}if(queries.length+mutations.length>1){if(process.env.NODE_ENV==="production"){throw new DataError("Too many ops",DataErrors.InvalidInput)}else {throw new DataError(`We only support one query or mutation per component. `+`${JSON.stringify(document)} had ${queries.length} queries and `+`${mutations.length} mutations. `,DataErrors.InvalidInput)}}const type=queries.length?DocumentTypes.query:DocumentTypes.mutation;const definitions=queries.length?queries:mutations;const definition=definitions[0];const variables=definition.variableDefinitions||[];const name=definition.name?.kind==="Name"?definition.name.value:"data";const payload={name,type,variables};cache.set(document,payload);return payload}
79
81
 
@@ -89,9 +91,12 @@ const useGqlRouterContext=(contextOverrides={})=>{const gqlRouterContext=React.u
89
91
 
90
92
  const useGql=(context={})=>{const gqlRouterContext=useGqlRouterContext(context);const gqlFetch=React.useCallback((operation,options=Object.freeze({}))=>{const{fetch,defaultContext}=gqlRouterContext;const{variables,context={}}=options;const finalContext=mergeGqlContext(defaultContext,context);return fetch(operation,variables,finalContext).then(response=>getGqlDataFromResponse(response))},[gqlRouterContext]);return gqlFetch};
91
93
 
94
+ const isErrorResultGqlError=error=>error instanceof ErrorResultGqlError;
95
+
92
96
  exports.Data = Data;
93
97
  exports.DataError = DataError;
94
98
  exports.DataErrors = DataErrors;
99
+ exports.ErrorResultGqlError = ErrorResultGqlError;
95
100
  exports.FetchPolicy = FetchPolicy;
96
101
  exports.GqlError = GqlError;
97
102
  exports.GqlErrors = GqlErrors;
@@ -110,6 +115,7 @@ exports.getGqlRequestId = getGqlRequestId;
110
115
  exports.graphQLDocumentNodeParser = graphQLDocumentNodeParser;
111
116
  exports.hasTrackedRequestsToBeFetched = hasTrackedRequestsToBeFetched;
112
117
  exports.initializeHydrationCache = initializeHydrationCache;
118
+ exports.isErrorResultGqlError = isErrorResultGqlError;
113
119
  exports.purgeCaches = purgeCaches;
114
120
  exports.purgeHydrationCache = purgeHydrationCache;
115
121
  exports.toGqlOperation = toGqlOperation;
@@ -0,0 +1,26 @@
1
+ import { GqlError } from "./gql-error";
2
+ import type { GqlErrorResultPayload } from "./gql-types";
3
+ /**
4
+ * A GraphQL response that completed but reported errors.
5
+ *
6
+ * GraphQL servers can return a valid response payload that carries both an
7
+ * `errors` array and a `data` object. Fields that failed are nulled, with the
8
+ * null propagating up to the nearest nullable ancestor, so the `data` that
9
+ * comes back is a partial version of the requested `TData`, or null if the
10
+ * whole result failed. This error exposes that payload with types so callers
11
+ * can make use of the partial data without guessing at the shape.
12
+ *
13
+ * The payload is also attached as `metadata` so that logging sees the same
14
+ * details it always did.
15
+ */
16
+ export declare class ErrorResultGqlError<TData> extends GqlError {
17
+ /**
18
+ * The HTTP status code of the response.
19
+ */
20
+ readonly statusCode: number;
21
+ /**
22
+ * The parsed response payload, including the partial data and errors.
23
+ */
24
+ readonly result: GqlErrorResultPayload<TData>;
25
+ constructor(statusCode: number, result: GqlErrorResultPayload<TData>);
26
+ }
@@ -32,3 +32,36 @@ export type GqlFetchOptions<TVariables extends Record<any, any>, TContext extend
32
32
  variables?: TVariables;
33
33
  context?: Partial<TContext>;
34
34
  };
35
+ /**
36
+ * The data of a GraphQL response that reported errors.
37
+ *
38
+ * Each top-level field is either the requested value or null. A field that
39
+ * errored is nulled, and that null propagates to the nearest nullable
40
+ * ancestor, so the top level of the result is the only level whose shape we
41
+ * can state with certainty.
42
+ */
43
+ export type GqlPartialData<TData> = {
44
+ [K in keyof TData]: TData[K] | null;
45
+ };
46
+ /**
47
+ * An error entry from the `errors` array of a GraphQL response.
48
+ */
49
+ export type GqlResponseError = {
50
+ message: string;
51
+ locations?: ReadonlyArray<{
52
+ line: number;
53
+ column: number;
54
+ }>;
55
+ path?: ReadonlyArray<string | number>;
56
+ extensions?: Record<string, unknown>;
57
+ };
58
+ /**
59
+ * The payload of a GraphQL response that reported errors.
60
+ *
61
+ * `data` is absent or null when the whole operation failed; otherwise it is
62
+ * the partial data the server was able to resolve.
63
+ */
64
+ export type GqlErrorResultPayload<TData> = {
65
+ data?: GqlPartialData<TData> | null;
66
+ errors: ReadonlyArray<GqlResponseError>;
67
+ };
@@ -0,0 +1,12 @@
1
+ import { ErrorResultGqlError } from "./error-result-gql-error";
2
+ /**
3
+ * Determine if an error is an `ErrorResultGqlError`.
4
+ *
5
+ * The runtime check cannot verify the shape of the partial data, so `TData`
6
+ * is asserted rather than proven. Callers should only supply a `TData` that
7
+ * matches the operation whose response produced the error.
8
+ *
9
+ * @param error The value to check.
10
+ * @returns True if the error is an `ErrorResultGqlError`; otherwise, false.
11
+ */
12
+ export declare const isErrorResultGqlError: <TData>(error: unknown) => error is ErrorResultGqlError<TData>;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "15.0.3",
6
+ "version": "15.1.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -20,7 +20,7 @@
20
20
  "module": "dist/es/index.js",
21
21
  "types": "dist/index.d.ts",
22
22
  "dependencies": {
23
- "@khanacademy/wonder-blocks-core": "12.4.4"
23
+ "@khanacademy/wonder-blocks-core": "12.5.0"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "@khanacademy/wonder-stuff-core": "^3.0.0",
@@ -29,7 +29,7 @@
29
29
  "devDependencies": {
30
30
  "@khanacademy/wonder-stuff-testing": "^4.0.3",
31
31
  "@khanacademy/wb-dev-build-settings": "3.3.0",
32
- "@khanacademy/wonder-blocks-testing-core": "5.1.1"
32
+ "@khanacademy/wonder-blocks-testing-core": "5.3.0"
33
33
  },
34
34
  "scripts": {
35
35
  "test": "echo \"Error: no test specified\" && exit 1"