@dhis2/app-service-data 3.17.0 → 3.17.1

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.
Files changed (34) hide show
  1. package/.gitignore +5 -0
  2. package/d2.config.js +9 -0
  3. package/jest.config.js +14 -0
  4. package/package.json +4 -4
  5. package/src/__tests__/integration.test.tsx +80 -0
  6. package/src/__tests__/mutations.test.tsx +71 -0
  7. package/src/index.ts +5 -0
  8. package/src/react/components/CustomDataProvider.tsx +33 -0
  9. package/src/react/components/DataMutation.tsx +24 -0
  10. package/src/react/components/DataProvider.test.tsx +22 -0
  11. package/src/react/components/DataProvider.tsx +58 -0
  12. package/src/react/components/DataQuery.tsx +26 -0
  13. package/src/react/components/index.ts +4 -0
  14. package/src/react/context/DataContext.tsx +5 -0
  15. package/src/react/context/defaultDataContext.test.ts +46 -0
  16. package/src/react/context/defaultDataContext.ts +9 -0
  17. package/src/react/hooks/index.ts +3 -0
  18. package/src/react/hooks/mergeAndCompareVariables.test.ts +65 -0
  19. package/src/react/hooks/mergeAndCompareVariables.ts +32 -0
  20. package/src/react/hooks/stableVariablesHash.test.ts +53 -0
  21. package/src/react/hooks/stableVariablesHash.ts +56 -0
  22. package/src/react/hooks/useDataEngine.ts +8 -0
  23. package/src/react/hooks/useDataMutation.test.tsx +296 -0
  24. package/src/react/hooks/useDataMutation.ts +42 -0
  25. package/src/react/hooks/useDataQuery.test.tsx +1029 -0
  26. package/src/react/hooks/useDataQuery.ts +170 -0
  27. package/src/react/hooks/useQueryExecutor.test.tsx +195 -0
  28. package/src/react/hooks/useQueryExecutor.ts +99 -0
  29. package/src/react/hooks/useStaticInput.test.ts +101 -0
  30. package/src/react/hooks/useStaticInput.ts +27 -0
  31. package/src/react/index.ts +2 -0
  32. package/src/setupRTL.ts +5 -0
  33. package/src/types.ts +72 -0
  34. package/tsconfig.json +10 -0
package/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ # DHIS2 Platform
2
+ node_modules
3
+ .d2
4
+ src/locales
5
+ build
package/d2.config.js ADDED
@@ -0,0 +1,9 @@
1
+ const config = {
2
+ type: 'lib',
3
+
4
+ entryPoints: {
5
+ lib: './src/index.ts',
6
+ },
7
+ }
8
+
9
+ module.exports = config
package/jest.config.js ADDED
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ collectCoverageFrom: [
3
+ 'src/**/*.(js|jsx|ts|tsx)',
4
+ '!src/index.ts',
5
+ '!src/**/types/*',
6
+ '!src/**/types.ts',
7
+ '!src/engine/index.ts',
8
+ '!src/links/index.ts',
9
+ '!src/react/index.ts',
10
+ ],
11
+
12
+ // Setup react-testing-library
13
+ setupFilesAfterEnv: ['<rootDir>/src/setupRTL.ts'],
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2/app-service-data",
3
- "version": "3.17.0",
3
+ "version": "3.17.1",
4
4
  "main": "./build/cjs/index.js",
5
5
  "module": "./build/es/index.js",
6
6
  "types": "./build/types/index.d.ts",
@@ -20,7 +20,7 @@
20
20
  "access": "public"
21
21
  },
22
22
  "files": [
23
- "build/**"
23
+ "**"
24
24
  ],
25
25
  "scripts": {
26
26
  "build:types": "tsc --emitDeclarationOnly --outDir ./build/types",
@@ -37,8 +37,8 @@
37
37
  "prop-types": "^15.7.2"
38
38
  },
39
39
  "peerDependencies": {
40
- "@dhis2/app-service-config": "3.17.0",
41
- "@dhis2/data-engine": "3.17.0",
40
+ "@dhis2/app-service-config": "3.17.1",
41
+ "@dhis2/data-engine": "3.17.1",
42
42
  "react": "^16.8.6 || ^18",
43
43
  "react-dom": "^16.8.6 || ^18"
44
44
  }
@@ -0,0 +1,80 @@
1
+ import { render, waitFor } from '@testing-library/react'
2
+ import * as React from 'react'
3
+ import { CustomDataProvider, DataQuery } from '../react'
4
+
5
+ describe('<DataQuery />', () => {
6
+ it('should render without failing', async () => {
7
+ const data = {
8
+ answer: 42,
9
+ }
10
+ const wrapper = ({ children }: { children?: React.ReactNode }) => (
11
+ <CustomDataProvider data={data}>{children}</CustomDataProvider>
12
+ )
13
+ const renderFunction = jest.fn(() => null)
14
+
15
+ render(
16
+ <DataQuery query={{ answer: { resource: 'answer' } }}>
17
+ {renderFunction}
18
+ </DataQuery>,
19
+ { wrapper }
20
+ )
21
+
22
+ expect(renderFunction).toHaveBeenCalledTimes(1)
23
+ expect(renderFunction).toHaveBeenLastCalledWith(
24
+ expect.objectContaining({
25
+ called: true,
26
+ loading: true,
27
+ })
28
+ )
29
+
30
+ await waitFor(() => {
31
+ expect(renderFunction).toHaveBeenCalledTimes(2)
32
+ expect(renderFunction).toHaveBeenLastCalledWith(
33
+ expect.objectContaining({
34
+ called: true,
35
+ loading: false,
36
+ data,
37
+ })
38
+ )
39
+ })
40
+ })
41
+
42
+ it('should render an error', async () => {
43
+ const expectedError = new Error('Something went wrong')
44
+ const data = {
45
+ test: () => {
46
+ throw expectedError
47
+ },
48
+ }
49
+ const wrapper = ({ children }: { children?: React.ReactNode }) => (
50
+ <CustomDataProvider data={data}>{children}</CustomDataProvider>
51
+ )
52
+ const renderFunction = jest.fn(() => null)
53
+
54
+ render(
55
+ <DataQuery query={{ test: { resource: 'test' } }}>
56
+ {renderFunction}
57
+ </DataQuery>,
58
+ { wrapper }
59
+ )
60
+
61
+ expect(renderFunction).toHaveBeenCalledTimes(1)
62
+ expect(renderFunction).toHaveBeenLastCalledWith(
63
+ expect.objectContaining({
64
+ called: true,
65
+ loading: true,
66
+ })
67
+ )
68
+
69
+ await waitFor(() => {
70
+ expect(renderFunction).toHaveBeenCalledTimes(2)
71
+ expect(renderFunction).toHaveBeenLastCalledWith(
72
+ expect.objectContaining({
73
+ called: true,
74
+ loading: false,
75
+ error: expectedError,
76
+ })
77
+ )
78
+ })
79
+ })
80
+ })
@@ -0,0 +1,71 @@
1
+ import { render, waitFor, act } from '@testing-library/react'
2
+ import * as React from 'react'
3
+ import { CustomDataProvider, DataMutation } from '../react'
4
+
5
+ describe('<DataMutation />', () => {
6
+ it('should render without failing', async () => {
7
+ const endpointSpy = jest.fn(() => Promise.resolve(42))
8
+ const mutation = {
9
+ resource: 'answer',
10
+ type: 'create' as const,
11
+ data: {
12
+ question: '?',
13
+ },
14
+ }
15
+ const data = {
16
+ answer: endpointSpy,
17
+ }
18
+ const wrapper = ({ children }: { children?: React.ReactNode }) => (
19
+ <CustomDataProvider data={data}>{children}</CustomDataProvider>
20
+ )
21
+ const renderSpy: jest.Mock = jest.fn(() => null)
22
+ render(<DataMutation mutation={mutation}>{renderSpy}</DataMutation>, {
23
+ wrapper,
24
+ })
25
+
26
+ expect(endpointSpy).toHaveBeenCalledTimes(0)
27
+ expect(renderSpy).toHaveBeenCalledTimes(1)
28
+ expect(renderSpy).toHaveBeenLastCalledWith([
29
+ expect.any(Function),
30
+ expect.objectContaining({
31
+ called: false,
32
+ loading: false,
33
+ engine: expect.any(Object),
34
+ }),
35
+ ])
36
+
37
+ await act(async () => {
38
+ const firstRenderSpyCall = renderSpy.mock.calls[0]
39
+ const firstRenderSpyArgument = firstRenderSpyCall[0]
40
+ const [mutate] = firstRenderSpyArgument
41
+ await mutate()
42
+ })
43
+
44
+ waitFor(() => {
45
+ expect(endpointSpy).toHaveBeenCalledTimes(1)
46
+ expect(renderSpy).toHaveBeenCalledTimes(2)
47
+ expect(renderSpy).toHaveBeenLastCalledWith([
48
+ expect.any(Function),
49
+ expect.objectContaining({
50
+ called: true,
51
+ loading: true,
52
+ engine: expect.any(Object),
53
+ }),
54
+ ])
55
+ })
56
+
57
+ waitFor(() => {
58
+ expect(endpointSpy).toHaveBeenCalledTimes(1)
59
+ expect(renderSpy).toHaveBeenCalledTimes(3)
60
+ expect(renderSpy).toHaveBeenLastCalledWith([
61
+ expect.any(Function),
62
+ expect.objectContaining({
63
+ called: true,
64
+ loading: false,
65
+ data: { answer: 42 },
66
+ engine: expect.any(Object),
67
+ }),
68
+ ])
69
+ })
70
+ })
71
+ })
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './react'
2
+
3
+ export { FetchError } from '@dhis2/data-engine'
4
+
5
+ export type * from './types'
@@ -0,0 +1,33 @@
1
+ import { DataEngine, CustomDataLink } from '@dhis2/data-engine'
2
+ import type { CustomData, CustomLinkOptions } from '@dhis2/data-engine'
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4
+ import React from 'react'
5
+ import { DataContext } from '../context/DataContext'
6
+ import { queryClientOptions as queryClientDefaults } from './DataProvider'
7
+
8
+ interface CustomProviderInput {
9
+ children: React.ReactNode
10
+ data: CustomData
11
+ options?: CustomLinkOptions
12
+ queryClientOptions?: any
13
+ }
14
+
15
+ export const CustomDataProvider = ({
16
+ children,
17
+ data,
18
+ options,
19
+ queryClientOptions = queryClientDefaults,
20
+ }: CustomProviderInput): JSX.Element => {
21
+ const link = new CustomDataLink(data, options)
22
+ const engine = new DataEngine(link)
23
+ const queryClient = new QueryClient(queryClientOptions)
24
+ const context = { engine }
25
+
26
+ return (
27
+ <QueryClientProvider client={queryClient}>
28
+ <DataContext.Provider value={context}>
29
+ {children}
30
+ </DataContext.Provider>
31
+ </QueryClientProvider>
32
+ )
33
+ }
@@ -0,0 +1,24 @@
1
+ import type { Mutation, MutationOptions } from '@dhis2/data-engine'
2
+ import { MutationRenderInput } from '../../types'
3
+ import { useDataMutation } from '../hooks/useDataMutation'
4
+
5
+ interface MutationInput extends MutationOptions {
6
+ mutation: Mutation
7
+ children: (input: MutationRenderInput) => any
8
+ }
9
+
10
+ export const DataMutation = ({
11
+ mutation,
12
+ onComplete,
13
+ onError,
14
+ variables,
15
+ children,
16
+ }: MutationInput) => {
17
+ const mutationState = useDataMutation(mutation, {
18
+ onComplete,
19
+ onError,
20
+ variables,
21
+ })
22
+
23
+ return children(mutationState)
24
+ }
@@ -0,0 +1,22 @@
1
+ import { DataEngine, RestAPILink } from '@dhis2/data-engine'
2
+ import { render } from '@testing-library/react'
3
+ import React from 'react'
4
+ import { DataContext } from '../context/DataContext'
5
+ import { DataProvider } from './DataProvider'
6
+
7
+ describe('DataProvider', () => {
8
+ it('Should pass a new engine and RestAPILink to consumers', () => {
9
+ const renderFunction = jest.fn()
10
+ render(
11
+ <DataProvider baseUrl="test" apiVersion={42}>
12
+ <DataContext.Consumer>{renderFunction}</DataContext.Consumer>
13
+ </DataProvider>
14
+ )
15
+
16
+ expect(renderFunction).toHaveBeenCalled()
17
+ const context = renderFunction.mock.calls.pop()[0]
18
+ expect(context).not.toBeUndefined()
19
+ expect(context.engine).toBeInstanceOf(DataEngine)
20
+ expect(context.engine.link).toBeInstanceOf(RestAPILink)
21
+ })
22
+ })
@@ -0,0 +1,58 @@
1
+ /* eslint-disable react/no-unused-prop-types */
2
+ import { useConfig } from '@dhis2/app-service-config'
3
+ import { DataEngine, RestAPILink } from '@dhis2/data-engine'
4
+ import {
5
+ QueryClient,
6
+ QueryClientProvider,
7
+ type QueryClientConfig,
8
+ } from '@tanstack/react-query'
9
+ import React from 'react'
10
+ import { DataContext } from '../context/DataContext'
11
+
12
+ export interface ProviderInput {
13
+ baseUrl?: string
14
+ apiVersion?: number
15
+ children: React.ReactNode
16
+ }
17
+
18
+ export const queryClientOptions: QueryClientConfig = {
19
+ defaultOptions: {
20
+ queries: {
21
+ // Disable automatic error retries
22
+ retry: false,
23
+ // Retry on mount if query has errored
24
+ retryOnMount: true,
25
+ // Refetch on mount if data is stale
26
+ refetchOnMount: true,
27
+ // Don't refetch when the window regains focus
28
+ refetchOnWindowFocus: false,
29
+ // Don't refetch after connection issues
30
+ refetchOnReconnect: false,
31
+ // RQv4 uses 'online' as the default, which pauses queries without network connection.
32
+ // 'always' reestablishes behavior from v3, and lets requests fire when offline
33
+ // https://tanstack.com/query/latest/docs/framework/react/guides/network-mode
34
+ networkMode: 'always',
35
+ },
36
+ },
37
+ }
38
+
39
+ const queryClient = new QueryClient(queryClientOptions)
40
+
41
+ export const DataProvider = (props: ProviderInput): JSX.Element => {
42
+ const config = {
43
+ ...useConfig(),
44
+ ...props,
45
+ }
46
+
47
+ const link = new RestAPILink(config)
48
+ const engine = new DataEngine(link)
49
+ const context = { engine }
50
+
51
+ return (
52
+ <QueryClientProvider client={queryClient}>
53
+ <DataContext.Provider value={context}>
54
+ {props.children}
55
+ </DataContext.Provider>
56
+ </QueryClientProvider>
57
+ )
58
+ }
@@ -0,0 +1,26 @@
1
+ import type { Query, QueryOptions } from '@dhis2/data-engine'
2
+ import { QueryRenderInput } from '../../types'
3
+ import { useDataQuery } from '../hooks/useDataQuery'
4
+
5
+ interface QueryInput extends QueryOptions {
6
+ query: Query
7
+ children: (input: QueryRenderInput) => any
8
+ }
9
+
10
+ export const DataQuery = ({
11
+ query,
12
+ onComplete,
13
+ onError,
14
+ variables,
15
+ lazy,
16
+ children,
17
+ }: QueryInput) => {
18
+ const queryState = useDataQuery(query, {
19
+ onComplete,
20
+ onError,
21
+ variables,
22
+ lazy,
23
+ })
24
+
25
+ return children(queryState)
26
+ }
@@ -0,0 +1,4 @@
1
+ export { CustomDataProvider } from './CustomDataProvider'
2
+ export { DataMutation } from './DataMutation'
3
+ export { DataProvider } from './DataProvider'
4
+ export { DataQuery } from './DataQuery'
@@ -0,0 +1,5 @@
1
+ import React from 'react'
2
+ import { ContextType } from '../../types'
3
+ import { defaultDataContext } from './defaultDataContext'
4
+
5
+ export const DataContext = React.createContext<ContextType>(defaultDataContext)
@@ -0,0 +1,46 @@
1
+ import { defaultDataContext } from './defaultDataContext'
2
+
3
+ describe('defaultContext', () => {
4
+ const originalError = console.error
5
+ const mockError = jest.fn()
6
+ beforeEach(() => {
7
+ jest.clearAllMocks()
8
+ console.error = mockError
9
+ })
10
+ afterEach(() => (console.error = originalError))
11
+
12
+ it('Should throw if query is called', () => {
13
+ const context = defaultDataContext
14
+ expect(
15
+ context.engine.query({
16
+ test: {
17
+ resource: 'test',
18
+ },
19
+ })
20
+ ).rejects.toBeTruthy()
21
+
22
+ expect(mockError).toHaveBeenCalledTimes(1)
23
+ expect(mockError.mock.calls.pop()).toMatchInlineSnapshot(`
24
+ Array [
25
+ "DHIS2 data context must be initialized, please ensure that you include a <DataProvider> in your application",
26
+ ]
27
+ `)
28
+ })
29
+
30
+ it('Should throw and log if mutate is called', () => {
31
+ const context = defaultDataContext
32
+ expect(
33
+ context.engine.mutate({
34
+ type: 'create',
35
+ resource: 'test',
36
+ data: {},
37
+ })
38
+ ).rejects.toBeTruthy()
39
+ expect(mockError).toHaveBeenCalled()
40
+ expect(mockError.mock.calls.pop()).toMatchInlineSnapshot(`
41
+ Array [
42
+ "DHIS2 data context must be initialized, please ensure that you include a <DataProvider> in your application",
43
+ ]
44
+ `)
45
+ })
46
+ })
@@ -0,0 +1,9 @@
1
+ import { DataEngine, ErrorLink } from '@dhis2/data-engine'
2
+
3
+ const errorMessage =
4
+ 'DHIS2 data context must be initialized, please ensure that you include a <DataProvider> in your application'
5
+
6
+ const link = new ErrorLink(errorMessage)
7
+ const engine = new DataEngine(link)
8
+
9
+ export const defaultDataContext = { engine }
@@ -0,0 +1,3 @@
1
+ export { useDataEngine } from './useDataEngine'
2
+ export { useDataMutation } from './useDataMutation'
3
+ export { useDataQuery } from './useDataQuery'
@@ -0,0 +1,65 @@
1
+ import { mergeAndCompareVariables } from './mergeAndCompareVariables'
2
+ import { stableVariablesHash } from './stableVariablesHash'
3
+ jest.mock('./stableVariablesHash', () => ({
4
+ stableVariablesHash: (object: unknown) => JSON.stringify(object),
5
+ }))
6
+
7
+ const testVariables = {
8
+ question: 'What do you get when you multiply six by nine?',
9
+ answer: 42,
10
+ }
11
+ const testHash = stableVariablesHash(testVariables)
12
+
13
+ describe('mergeAndCompareVariables', () => {
14
+ it('Should return previous variables and hash when no new variables are provided', () => {
15
+ expect(
16
+ mergeAndCompareVariables(testVariables, undefined, undefined)
17
+ ).toMatchObject({
18
+ identical: true,
19
+ mergedVariables: testVariables,
20
+ mergedVariablesHash: undefined,
21
+ })
22
+ })
23
+
24
+ it('Should return identical: true when merged variables are identical to old variables (without prev hash)', () => {
25
+ const newVariables = { answer: testVariables.answer }
26
+
27
+ expect(
28
+ mergeAndCompareVariables(testVariables, newVariables, undefined)
29
+ ).toMatchObject({
30
+ identical: true,
31
+ mergedVariables: testVariables,
32
+ mergedVariablesHash: testHash,
33
+ })
34
+ })
35
+
36
+ it('Should return identical: false with incorrect previous hash', () => {
37
+ const incorrectPreviousHash = 'IAmAHash'
38
+ const newVariables = { answer: 42 }
39
+
40
+ expect(
41
+ mergeAndCompareVariables(
42
+ testVariables,
43
+ newVariables,
44
+ incorrectPreviousHash
45
+ )
46
+ ).toMatchObject({
47
+ identical: false,
48
+ mergedVariables: testVariables,
49
+ mergedVariablesHash: testHash,
50
+ })
51
+ })
52
+
53
+ it('Should return identical: false when merged variables are different than old variables', () => {
54
+ const newVariables = { answer: 43 }
55
+ const expectedMergedVariables = { ...testVariables, ...newVariables }
56
+
57
+ expect(
58
+ mergeAndCompareVariables(testVariables, newVariables, testHash)
59
+ ).toMatchObject({
60
+ identical: false,
61
+ mergedVariables: expectedMergedVariables,
62
+ mergedVariablesHash: stableVariablesHash(expectedMergedVariables),
63
+ })
64
+ })
65
+ })
@@ -0,0 +1,32 @@
1
+ import type { QueryVariables } from '@dhis2/data-engine'
2
+ import { stableVariablesHash } from './stableVariablesHash'
3
+
4
+ export const mergeAndCompareVariables = (
5
+ previousVariables?: QueryVariables,
6
+ newVariables?: QueryVariables,
7
+ previousHash?: string
8
+ ) => {
9
+ if (!newVariables) {
10
+ return {
11
+ identical: true,
12
+ mergedVariablesHash: previousHash,
13
+ mergedVariables: previousVariables,
14
+ }
15
+ }
16
+
17
+ // Use cached hash if it exists
18
+ const currentHash = previousHash || stableVariablesHash(previousVariables)
19
+
20
+ const mergedVariables = {
21
+ ...previousVariables,
22
+ ...newVariables,
23
+ }
24
+ const mergedVariablesHash = stableVariablesHash(mergedVariables)
25
+ const identical = currentHash === mergedVariablesHash
26
+
27
+ return {
28
+ identical,
29
+ mergedVariablesHash,
30
+ mergedVariables,
31
+ }
32
+ }
@@ -0,0 +1,53 @@
1
+ import { stableVariablesHash } from './stableVariablesHash'
2
+
3
+ describe('stableVariablesHash', () => {
4
+ it('sorts objects before hashing', () => {
5
+ const one = {
6
+ a: {
7
+ one: 1,
8
+ two: 2,
9
+ three: 3,
10
+ },
11
+ b: [1, 2, 3],
12
+ c: 'c',
13
+ }
14
+ const two = {
15
+ c: 'c',
16
+ b: [1, 2, 3],
17
+ a: {
18
+ three: 3,
19
+ two: 2,
20
+ one: 1,
21
+ },
22
+ }
23
+
24
+ expect(stableVariablesHash(one)).toEqual(stableVariablesHash(two))
25
+ })
26
+
27
+ it('can handle primitives', () => {
28
+ const one = undefined
29
+ const two = 'string'
30
+ const three = 3
31
+ const four = null
32
+ const five = true
33
+
34
+ expect(stableVariablesHash(one)).toMatchInlineSnapshot(`undefined`)
35
+ expect(stableVariablesHash(two)).toMatchInlineSnapshot(`"\\"string\\""`)
36
+ expect(stableVariablesHash(three)).toMatchInlineSnapshot(`"3"`)
37
+ expect(stableVariablesHash(four)).toMatchInlineSnapshot(`"null"`)
38
+ expect(stableVariablesHash(five)).toMatchInlineSnapshot(`"true"`)
39
+ })
40
+
41
+ it('throws a clear error when the variables contain a circular reference', () => {
42
+ const unserializable: any = {
43
+ value: 'value',
44
+ }
45
+ unserializable.circular = unserializable
46
+
47
+ expect(() =>
48
+ stableVariablesHash(unserializable)
49
+ ).toThrowErrorMatchingInlineSnapshot(
50
+ `"Could not serialize variables. Make sure that the variables do not contain circular references and can be processed by JSON.stringify."`
51
+ )
52
+ })
53
+ })
@@ -0,0 +1,56 @@
1
+ function hasObjectPrototype(o: any): boolean {
2
+ return Object.prototype.toString.call(o) === '[object Object]'
3
+ }
4
+
5
+ export function isPlainObject(o: any): o is object {
6
+ if (!hasObjectPrototype(o)) {
7
+ return false
8
+ }
9
+
10
+ // If has modified constructor
11
+ const ctor = o.constructor
12
+ if (typeof ctor === 'undefined') {
13
+ return true
14
+ }
15
+
16
+ // If has modified prototype
17
+ const prot = ctor.prototype
18
+ if (!hasObjectPrototype(prot)) {
19
+ return false
20
+ }
21
+
22
+ // If constructor does not have an Object-specific method
23
+ if (!Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf')) {
24
+ return false
25
+ }
26
+
27
+ // Most likely a plain Object
28
+ return true
29
+ }
30
+
31
+ /**
32
+ * Hashes the value into a stable hash.
33
+ */
34
+
35
+ export function stableVariablesHash(value: any): string {
36
+ let hash
37
+
38
+ try {
39
+ hash = JSON.stringify(value, (_, val) =>
40
+ isPlainObject(val)
41
+ ? Object.keys(val)
42
+ .sort()
43
+ .reduce((result: Record<string, unknown>, key) => {
44
+ result[key] = (val as Record<string, unknown>)[key]
45
+ return result
46
+ }, {})
47
+ : val
48
+ )
49
+ } catch (e) {
50
+ throw new Error(
51
+ 'Could not serialize variables. Make sure that the variables do not contain circular references and can be processed by JSON.stringify.'
52
+ )
53
+ }
54
+
55
+ return hash
56
+ }
@@ -0,0 +1,8 @@
1
+ import { useContext } from 'react'
2
+ import { DataContext } from '../context/DataContext'
3
+
4
+ export const useDataEngine = () => {
5
+ const context = useContext(DataContext)
6
+
7
+ return context.engine
8
+ }