From 0516b041b7a1b626f57a16991445c517bd43602f Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 27 Nov 2024 10:33:14 +0200 Subject: [PATCH 1/2] Fixed error handling for Tanstack wrapper. Added default empty array for parameters option. Added unit tests for Tanstack wrapper. --- .changeset/brown-moose-chew.md | 5 + packages/tanstack-react-query/package.json | 2 + .../src/hooks/useQuery.ts | 25 ++-- .../tanstack-react-query/tests/tsconfig.json | 22 ++++ .../tests/useQuery.test.tsx | 114 ++++++++++++++++++ .../tanstack-react-query/vitest.config.ts | 9 ++ pnpm-lock.yaml | 41 ++++--- 7 files changed, 184 insertions(+), 34 deletions(-) create mode 100644 .changeset/brown-moose-chew.md create mode 100644 packages/tanstack-react-query/tests/tsconfig.json create mode 100644 packages/tanstack-react-query/tests/useQuery.test.tsx create mode 100644 packages/tanstack-react-query/vitest.config.ts diff --git a/.changeset/brown-moose-chew.md b/.changeset/brown-moose-chew.md new file mode 100644 index 000000000..ae544869a --- /dev/null +++ b/.changeset/brown-moose-chew.md @@ -0,0 +1,5 @@ +--- +'@powersync/tanstack-react-query': patch +--- + +Fixed issue with compilable queries needing a parameter value specified and fixed issue related to compilable query errors causing infinite rendering. diff --git a/packages/tanstack-react-query/package.json b/packages/tanstack-react-query/package.json index 45a618c9d..4778b4fb6 100644 --- a/packages/tanstack-react-query/package.json +++ b/packages/tanstack-react-query/package.json @@ -15,6 +15,7 @@ "build": "tsc -b", "build:prod": "tsc -b --sourceMap false", "clean": "rm -rf lib tsconfig.tsbuildinfo", + "test": "vitest", "watch": "tsc -b -w" }, "repository": { @@ -37,6 +38,7 @@ "@tanstack/react-query": "^5.55.4" }, "devDependencies": { + "@testing-library/react": "^15.0.2", "@types/react": "^18.2.34", "jsdom": "^24.0.0", "react": "18.2.0", diff --git a/packages/tanstack-react-query/src/hooks/useQuery.ts b/packages/tanstack-react-query/src/hooks/useQuery.ts index 8c24bc569..aa1034ed4 100644 --- a/packages/tanstack-react-query/src/hooks/useQuery.ts +++ b/packages/tanstack-react-query/src/hooks/useQuery.ts @@ -1,4 +1,4 @@ -import { parseQuery, type CompilableQuery, type ParsedQuery, type SQLWatchOptions } from '@powersync/common'; +import { parseQuery, type CompilableQuery } from '@powersync/common'; import { usePowerSync } from '@powersync/react'; import React from 'react'; @@ -65,15 +65,10 @@ function useQueryCore< throw new Error('PowerSync is not available'); } - const [error, setError] = React.useState(null); - const [tables, setTables] = React.useState([]); - const { query, parameters, ...resolvedOptions } = options; + let error: Error | undefined = undefined; - React.useEffect(() => { - if (error) { - setError(null); - } - }, [powerSync, query, parameters, options.queryKey]); + const [tables, setTables] = React.useState([]); + const { query, parameters = [], ...resolvedOptions } = options; let sqlStatement = ''; let queryParameters = []; @@ -85,7 +80,7 @@ function useQueryCore< sqlStatement = parsedQuery.sqlStatement; queryParameters = parsedQuery.parameters; } catch (e) { - setError(e); + error = e; } } @@ -97,12 +92,12 @@ function useQueryCore< const tables = await powerSync.resolveTables(sqlStatement, queryParameters); setTables(tables); } catch (e) { - setError(e); + error = e; } }; React.useEffect(() => { - if (!query) return () => {}; + if (error || !query) return () => {}; (async () => { await fetchTables(); @@ -128,7 +123,7 @@ function useQueryCore< } catch (e) { return Promise.reject(e); } - }, [powerSync, query, parameters, stringifiedKey, error]); + }, [powerSync, query, parameters, stringifiedKey]); React.useEffect(() => { if (error || !query) return () => {}; @@ -142,7 +137,7 @@ function useQueryCore< }); }, onError: (e) => { - setError(e); + error = e; } }, { @@ -151,7 +146,7 @@ function useQueryCore< } ); return () => abort.abort(); - }, [powerSync, queryClient, stringifiedKey, tables, error]); + }, [powerSync, queryClient, stringifiedKey, tables]); return useQueryFn( { diff --git a/packages/tanstack-react-query/tests/tsconfig.json b/packages/tanstack-react-query/tests/tsconfig.json new file mode 100644 index 000000000..41af59e85 --- /dev/null +++ b/packages/tanstack-react-query/tests/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "esModuleInterop": true, + "jsx": "react", + "rootDir": "../", + "composite": true, + "outDir": "./lib", + "lib": ["esnext", "DOM"], + "module": "esnext", + "sourceMap": true, + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "esnext" + }, + "include": ["../src/**/*"] +} diff --git a/packages/tanstack-react-query/tests/useQuery.test.tsx b/packages/tanstack-react-query/tests/useQuery.test.tsx new file mode 100644 index 000000000..6ce369493 --- /dev/null +++ b/packages/tanstack-react-query/tests/useQuery.test.tsx @@ -0,0 +1,114 @@ +import * as commonSdk from '@powersync/common'; +import { cleanup, renderHook, waitFor } from '@testing-library/react'; +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { PowerSyncContext } from '@powersync/react/'; +import { useQuery } from '../src/hooks/useQuery'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const mockPowerSync = { + currentStatus: { status: 'initial' }, + registerListener: vi.fn(() => { }), + resolveTables: vi.fn(() => ['table1', 'table2']), + onChangeWithCallback: vi.fn(), + getAll: vi.fn(() => Promise.resolve(['list1', 'list2'])) +}; + +vi.mock('./PowerSyncContext', () => ({ + useContext: vi.fn(() => mockPowerSync) +})); + +describe('useQuery', () => { + let queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + } + }) + + const wrapper = ({ children }) => ( + + {children} + + ); + + beforeEach(() => { + queryClient.clear(); + + vi.clearAllMocks(); + cleanup(); // Cleanup the DOM after each test + }); + + + it('should set loading states on initial load', async () => { + const { result } = renderHook(() => useQuery({ + queryKey: ['lists'], + query: 'SELECT * from lists' + }), { wrapper }); + const currentResult = result.current; + expect(currentResult.isLoading).toEqual(true); + expect(currentResult.isFetching).toEqual(true); + }); + + it('should execute string queries', async () => { + const query = () => + useQuery({ + queryKey: ['lists'], + query: "SELECT * from lists" + }); + const { result } = renderHook(query, { wrapper }); + + await vi.waitFor(() => { + expect(result.current.data![0]).toEqual('list1'); + expect(result.current.data![1]).toEqual('list2'); + }, { timeout: 500 }); + }); + + it('should execute compatible queries', async () => { + const compilableQuery = { + execute: () => [{ test: 'custom' }] as any, + compile: () => ({ sql: 'SELECT * from lists' }) + } as commonSdk.CompilableQuery; + + const query = () => + useQuery({ + queryKey: ['lists'], + query: compilableQuery + }); + const { result } = renderHook(query, { wrapper }); + + await vi.waitFor(() => { + expect(result.current.data![0].test).toEqual('custom'); + }, { timeout: 500 }); + }); + + it('should show an error if parsing the query results in an error', async () => { + const compilableQuery = { + execute: () => [] as any, + compile: () => ({ sql: 'SELECT * from lists', parameters: ['param'] }) + } as commonSdk.CompilableQuery; + + const { result } = renderHook( + () => + useQuery({ + queryKey: ['lists'], + query: compilableQuery, + parameters: ['redundant param'] + }), + { wrapper } + ); + + await waitFor( + async () => { + const currentResult = result.current; + expect(currentResult.isLoading).toEqual(false); + expect(currentResult.isFetching).toEqual(false); + expect(currentResult.error).toEqual(Error('You cannot pass parameters to a compiled query.')); + expect(currentResult.data).toBeUndefined() + }, + { timeout: 100 } + ); + }); + +}); diff --git a/packages/tanstack-react-query/vitest.config.ts b/packages/tanstack-react-query/vitest.config.ts new file mode 100644 index 000000000..f96ac5630 --- /dev/null +++ b/packages/tanstack-react-query/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, UserConfigExport } from 'vitest/config'; + +const config: UserConfigExport = { + test: { + environment: 'jsdom' + } +}; + +export default defineConfig(config); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b88539094..e4464969b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,16 +234,16 @@ importers: dependencies: '@capacitor/android': specifier: ^6.0.0 - version: 6.1.2(@capacitor/core@6.1.2) + version: 6.1.2(@capacitor/core@6.2.0) '@capacitor/core': specifier: latest - version: 6.1.2 + version: 6.2.0 '@capacitor/ios': specifier: ^6.0.0 - version: 6.1.2(@capacitor/core@6.1.2) + version: 6.1.2(@capacitor/core@6.2.0) '@capacitor/splash-screen': specifier: latest - version: 6.0.2(@capacitor/core@6.1.2) + version: 6.0.3(@capacitor/core@6.2.0) '@journeyapps/wa-sqlite': specifier: ^1.0.0 version: 1.0.0 @@ -1843,6 +1843,9 @@ importers: specifier: ^5.55.4 version: 5.59.14(react@18.2.0) devDependencies: + '@testing-library/react': + specifier: ^15.0.2 + version: 15.0.7(@types/react@18.3.11)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/react': specifier: ^18.2.34 version: 18.3.11 @@ -3199,16 +3202,16 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - '@capacitor/core@6.1.2': - resolution: {integrity: sha512-xFy1/4qLFLp5WCIzIhtwUuVNNoz36+V7/BzHmLqgVJcvotc4MMjswW/TshnPQaLLujEOaLkA4h8ZJ0uoK3ImGg==} + '@capacitor/core@6.2.0': + resolution: {integrity: sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==} '@capacitor/ios@6.1.2': resolution: {integrity: sha512-HaeW68KisBd/7TmavzPDlL2bpoDK5AjR2ZYrqU4TlGwM88GtQfvduBCAlSCj20X0w/4+rWMkseD9dAAkacjiyQ==} peerDependencies: '@capacitor/core': ^6.1.0 - '@capacitor/splash-screen@6.0.2': - resolution: {integrity: sha512-WC0KYZ+ev15up03xs4fTnoTKwBVUSxXsKKQr/8XAncvi/nAG8qrpanW8OlavSC5zF5e1IZZDLsI2GSv0SkZ7VQ==} + '@capacitor/splash-screen@6.0.3': + resolution: {integrity: sha512-tpVljeNGSwVCIc8lMQkyiCQFokk2PwgYPdDtPnGjFthqmXW/WhIxW8QYl4MUqyLwwgwTEbp4u3Kcv2zqQu2L6Q==} peerDependencies: '@capacitor/core': ^6.0.0 @@ -19100,9 +19103,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/eslint-parser@7.25.8(@babel/core@7.24.5)(eslint@8.57.1)': + '@babel/eslint-parser@7.25.8(@babel/core@7.25.7)(eslint@8.57.1)': dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.25.7 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 eslint: 8.57.1 eslint-visitor-keys: 2.1.0 @@ -21542,9 +21545,9 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 - '@capacitor/android@6.1.2(@capacitor/core@6.1.2)': + '@capacitor/android@6.1.2(@capacitor/core@6.2.0)': dependencies: - '@capacitor/core': 6.1.2 + '@capacitor/core': 6.2.0 '@capacitor/cli@6.1.2': dependencies: @@ -21569,17 +21572,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@capacitor/core@6.1.2': + '@capacitor/core@6.2.0': dependencies: tslib: 2.7.0 - '@capacitor/ios@6.1.2(@capacitor/core@6.1.2)': + '@capacitor/ios@6.1.2(@capacitor/core@6.2.0)': dependencies: - '@capacitor/core': 6.1.2 + '@capacitor/core': 6.2.0 - '@capacitor/splash-screen@6.0.2(@capacitor/core@6.1.2)': + '@capacitor/splash-screen@6.0.3(@capacitor/core@6.2.0)': dependencies: - '@capacitor/core': 6.1.2 + '@capacitor/core': 6.2.0 '@changesets/apply-release-plan@7.0.5': dependencies: @@ -26897,7 +26900,7 @@ snapshots: '@react-native/eslint-config@0.73.2(eslint@8.57.1)(prettier@3.3.3)(typescript@5.5.4)': dependencies: '@babel/core': 7.24.5 - '@babel/eslint-parser': 7.25.8(@babel/core@7.24.5)(eslint@8.57.1) + '@babel/eslint-parser': 7.25.8(@babel/core@7.25.7)(eslint@8.57.1) '@react-native/eslint-plugin': 0.73.1 '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1)(typescript@5.5.4) '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.5.4) @@ -33377,7 +33380,7 @@ snapshots: eslint-plugin-ft-flow@2.0.3(@babel/eslint-parser@7.25.8(@babel/core@7.24.5)(eslint@8.57.1))(eslint@8.57.1): dependencies: - '@babel/eslint-parser': 7.25.8(@babel/core@7.24.5)(eslint@8.57.1) + '@babel/eslint-parser': 7.25.8(@babel/core@7.25.7)(eslint@8.57.1) eslint: 8.57.1 lodash: 4.17.21 string-natural-compare: 3.0.1 From c6fd4a62df5e149d4afb1f6d606c5efb132e810a Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 27 Nov 2024 13:06:25 +0200 Subject: [PATCH 2/2] Added test for confirming error on query execution. --- .../tests/useQuery.test.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/tanstack-react-query/tests/useQuery.test.tsx b/packages/tanstack-react-query/tests/useQuery.test.tsx index 6ce369493..e6aa2fd60 100644 --- a/packages/tanstack-react-query/tests/useQuery.test.tsx +++ b/packages/tanstack-react-query/tests/useQuery.test.tsx @@ -65,6 +65,36 @@ describe('useQuery', () => { }, { timeout: 500 }); }); + it('should set error during query execution', async () => { + const mockPowerSyncError = { + currentStatus: { status: 'initial' }, + registerListener: vi.fn(() => { }), + onChangeWithCallback: vi.fn(), + resolveTables: vi.fn(() => ['table1', 'table2']), + getAll: vi.fn(() => { + throw new Error('some error'); + }) + }; + + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useQuery({ + queryKey: ['lists'], + query: 'SELECT * from lists' + }), { wrapper }); + + await waitFor( + async () => { + expect(result.current.error).toEqual(Error('some error')); + }, + { timeout: 100 } + ); + }); + it('should execute compatible queries', async () => { const compilableQuery = { execute: () => [{ test: 'custom' }] as any,