@khanacademy/wonder-blocks-data 9.0.0 → 9.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,11 @@
1
1
  # @khanacademy/wonder-blocks-data
2
2
 
3
+ ## 9.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 944c3071: Make sure objects and arrays in variables are sorted and readable in generated request identifiers
8
+
3
9
  ## 9.0.0
4
10
 
5
11
  ### Major Changes
package/dist/es/index.js CHANGED
@@ -683,14 +683,27 @@ const InterceptRequests = ({
683
683
  }, children);
684
684
  };
685
685
 
686
- const toString = valid => {
686
+ const toString = value => {
687
687
  var _JSON$stringify;
688
688
 
689
- if (typeof valid === "string") {
690
- return valid;
689
+ if (typeof value === "string") {
690
+ return value;
691
691
  }
692
692
 
693
- return (_JSON$stringify = JSON.stringify(valid)) != null ? _JSON$stringify : "";
693
+ return (_JSON$stringify = JSON.stringify(value)) != null ? _JSON$stringify : "";
694
+ };
695
+
696
+ const toStringifiedVariables = (acc, key, value) => {
697
+ if (typeof value === "object" && value !== null) {
698
+ return Object.entries(value).reduce((innerAcc, [i, v]) => {
699
+ const subKey = `${key}.${i}`;
700
+ return toStringifiedVariables(innerAcc, subKey, v);
701
+ }, acc);
702
+ } else {
703
+ acc[key] = toString(value);
704
+ }
705
+
706
+ return acc;
694
707
  };
695
708
 
696
709
  const getGqlRequestId = (operation, variables, context) => {
@@ -702,8 +715,8 @@ const getGqlRequestId = (operation, variables, context) => {
702
715
 
703
716
  if (variables != null) {
704
717
  const stringifiedVariables = Object.keys(variables).reduce((acc, key) => {
705
- acc[key] = toString(variables[key]);
706
- return acc;
718
+ const value = variables[key];
719
+ return toStringifiedVariables(acc, key, value);
707
720
  }, {});
708
721
  const sortableVariables = new URLSearchParams(stringifiedVariables);
709
722
  sortableVariables.sort();
package/dist/index.js CHANGED
@@ -1843,14 +1843,32 @@ const InterceptRequests = ({
1843
1843
 
1844
1844
  "use strict";
1845
1845
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return getGqlRequestId; });
1846
- const toString = valid => {
1846
+ const toString = value => {
1847
1847
  var _JSON$stringify;
1848
1848
 
1849
- if (typeof valid === "string") {
1850
- return valid;
1849
+ if (typeof value === "string") {
1850
+ return value;
1851
1851
  }
1852
1852
 
1853
- return (_JSON$stringify = JSON.stringify(valid)) != null ? _JSON$stringify : "";
1853
+ return (_JSON$stringify = JSON.stringify(value)) != null ? _JSON$stringify : "";
1854
+ };
1855
+
1856
+ const toStringifiedVariables = (acc, key, value) => {
1857
+ if (typeof value === "object" && value !== null) {
1858
+ // If we have an object or array, we build sub-variables so that
1859
+ // the ID is easily human-readable rather than having lots of
1860
+ // extra %-encodings. This means that an object or array variable
1861
+ // turns into x variables, where x is the field or element count of
1862
+ // variable. See below for example.
1863
+ return Object.entries(value).reduce((innerAcc, [i, v]) => {
1864
+ const subKey = `${key}.${i}`;
1865
+ return toStringifiedVariables(innerAcc, subKey, v);
1866
+ }, acc);
1867
+ } else {
1868
+ acc[key] = toString(value);
1869
+ }
1870
+
1871
+ return acc;
1854
1872
  };
1855
1873
  /**
1856
1874
  * Get an identifier for a given request.
@@ -1871,9 +1889,36 @@ const getGqlRequestId = (operation, variables, context) => {
1871
1889
 
1872
1890
  if (variables != null) {
1873
1891
  // We need to turn each variable into a string.
1892
+ // We also need to ensure we sort any sub-object keys.
1893
+ // `toStringifiedVariables` helps us with this by hoisting nested
1894
+ // data to individual variables for the purposes of ID generation.
1895
+ //
1896
+ // For example, consider variables:
1897
+ // {x: [1,2,3], y: {a: 1, b: 2, c: 3}, z: 123}
1898
+ //
1899
+ // Each variable, x, y and z, would be stringified into
1900
+ // stringifiedVariables as follows:
1901
+ // x becomes {"x.0": "1", "x.1": "2", "x.2": "3"}
1902
+ // y becomes {"y.a": "1", "y.b": "2", "y.c": "3"}
1903
+ // z becomes {"z": "123"}
1904
+ //
1905
+ // This then leads to stringifiedVariables being:
1906
+ // {
1907
+ // "x.0": "1",
1908
+ // "x.1": "2",
1909
+ // "x.2": "3",
1910
+ // "y.a": "1",
1911
+ // "y.b": "2",
1912
+ // "y.c": "3",
1913
+ // "z": "123",
1914
+ // }
1915
+ //
1916
+ // Thus allowing our use of URLSearchParams to both sort and easily
1917
+ // encode the variables into an idempotent identifier for those
1918
+ // variable values that is also human-readable.
1874
1919
  const stringifiedVariables = Object.keys(variables).reduce((acc, key) => {
1875
- acc[key] = toString(variables[key]);
1876
- return acc;
1920
+ const value = variables[key];
1921
+ return toStringifiedVariables(acc, key, value);
1877
1922
  }, {}); // We use the same mechanism as context to sort and arrange the
1878
1923
  // variables.
1879
1924
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-data",
3
- "version": "9.0.0",
3
+ "version": "9.1.0",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -71,4 +71,37 @@ describe("#getGqlRequestId", () => {
71
71
  `variable1=value1&variable2=42&variable3=&variable4=null&variable5=true`,
72
72
  );
73
73
  });
74
+
75
+ it("should sort nested variable properties", () => {
76
+ // Arrange
77
+ const operation = {
78
+ type: "query",
79
+ id: "myQuery",
80
+ };
81
+ const variables = {
82
+ variable4: null,
83
+ variable2: 42,
84
+ variable1: "value1",
85
+ variable5: true,
86
+ variable3: undefined,
87
+ variable6: {
88
+ nested2: "nested2",
89
+ nested1: "nested1",
90
+ },
91
+ variable7: [1, 2, 3],
92
+ };
93
+
94
+ // Act
95
+ const requestId = getGqlRequestId(operation, variables, {
96
+ module: "MODULE",
97
+ curriculum: "CURRICULUM",
98
+ targetLocale: "LOCALE",
99
+ });
100
+ const result = new Set(requestId.split("|"));
101
+
102
+ // Assert
103
+ expect(result).toContain(
104
+ `variable1=value1&variable2=42&variable3=&variable4=null&variable5=true&variable6.nested1=nested1&variable6.nested2=nested2&variable7.0=1&variable7.1=2&variable7.2=3`,
105
+ );
106
+ });
74
107
  });
@@ -1,11 +1,28 @@
1
1
  // @flow
2
2
  import type {GqlOperation, GqlContext} from "./gql-types.js";
3
3
 
4
- const toString = (valid: mixed): string => {
5
- if (typeof valid === "string") {
6
- return valid;
4
+ const toString = (value: mixed): string => {
5
+ if (typeof value === "string") {
6
+ return value;
7
7
  }
8
- return JSON.stringify(valid) ?? "";
8
+ return JSON.stringify(value) ?? "";
9
+ };
10
+
11
+ const toStringifiedVariables = (acc: any, key: string, value: mixed): any => {
12
+ if (typeof value === "object" && value !== null) {
13
+ // If we have an object or array, we build sub-variables so that
14
+ // the ID is easily human-readable rather than having lots of
15
+ // extra %-encodings. This means that an object or array variable
16
+ // turns into x variables, where x is the field or element count of
17
+ // variable. See below for example.
18
+ return Object.entries(value).reduce((innerAcc, [i, v]) => {
19
+ const subKey = `${key}.${i}`;
20
+ return toStringifiedVariables(innerAcc, subKey, v);
21
+ }, acc);
22
+ } else {
23
+ acc[key] = toString(value);
24
+ }
25
+ return acc;
9
26
  };
10
27
 
11
28
  /**
@@ -32,10 +49,37 @@ export const getGqlRequestId = <TData, TVariables: {...}>(
32
49
  // Finally, if we have variables, we add those too.
33
50
  if (variables != null) {
34
51
  // We need to turn each variable into a string.
52
+ // We also need to ensure we sort any sub-object keys.
53
+ // `toStringifiedVariables` helps us with this by hoisting nested
54
+ // data to individual variables for the purposes of ID generation.
55
+ //
56
+ // For example, consider variables:
57
+ // {x: [1,2,3], y: {a: 1, b: 2, c: 3}, z: 123}
58
+ //
59
+ // Each variable, x, y and z, would be stringified into
60
+ // stringifiedVariables as follows:
61
+ // x becomes {"x.0": "1", "x.1": "2", "x.2": "3"}
62
+ // y becomes {"y.a": "1", "y.b": "2", "y.c": "3"}
63
+ // z becomes {"z": "123"}
64
+ //
65
+ // This then leads to stringifiedVariables being:
66
+ // {
67
+ // "x.0": "1",
68
+ // "x.1": "2",
69
+ // "x.2": "3",
70
+ // "y.a": "1",
71
+ // "y.b": "2",
72
+ // "y.c": "3",
73
+ // "z": "123",
74
+ // }
75
+ //
76
+ // Thus allowing our use of URLSearchParams to both sort and easily
77
+ // encode the variables into an idempotent identifier for those
78
+ // variable values that is also human-readable.
35
79
  const stringifiedVariables = Object.keys(variables).reduce(
36
80
  (acc, key) => {
37
- acc[key] = toString(variables[key]);
38
- return acc;
81
+ const value = variables[key];
82
+ return toStringifiedVariables(acc, key, value);
39
83
  },
40
84
  {},
41
85
  );