Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] Custom Query Executor Hooks #295

Merged
merged 3 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nervous-ducks-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/kysely-driver': minor
---

Made `dialect` in `wrapPowerSyncWithKysely` options optional since the method provides a PowerSync dialect by default.
6 changes: 6 additions & 0 deletions .changeset/orange-points-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@powersync/react': patch
'@powersync/vue': patch
---

React and Vue helpers should execute queries from compatible query executor methods. This should allow Kysely queries with plugins to function correctly.
13 changes: 10 additions & 3 deletions packages/kysely-driver/src/sqlite/db.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { PowerSyncDialect } from './sqlite-dialect';
import { Kysely, type KyselyConfig } from 'kysely';
import { type AbstractPowerSyncDatabase } from '@powersync/common';
import { Dialect, Kysely, type KyselyConfig } from 'kysely';
import { PowerSyncDialect } from './sqlite-dialect';

/**
* An extension of {@link KyselyConfig} which uses the {@link PowerSyncDialect} by default.
*/
export type PowerSyncKyselyOptions = Omit<KyselyConfig, 'dialect'> & {
dialect?: Dialect;
};

export const wrapPowerSyncWithKysely = <T>(db: AbstractPowerSyncDatabase, options?: KyselyConfig) => {
export const wrapPowerSyncWithKysely = <T>(db: AbstractPowerSyncDatabase, options?: PowerSyncKyselyOptions) => {
return new Kysely<T>({
dialect: new PowerSyncDialect({
db
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/hooks/useQuery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type SQLWatchOptions, parseQuery, type CompilableQuery, type ParsedQuery } from '@powersync/common';
import { parseQuery, type CompilableQuery, type ParsedQuery, type SQLWatchOptions } from '@powersync/common';
import React from 'react';
import { usePowerSync } from './PowerSyncContext';

Expand Down Expand Up @@ -85,7 +85,8 @@ export const useQuery = <T = any>(
const fetchData = async () => {
setIsFetching(true);
try {
const result = await powerSync.getAll<T>(sqlStatement, queryParameters);
const result =
typeof query == 'string' ? await powerSync.getAll<T>(sqlStatement, queryParameters) : await query.execute();
handleResult(result);
} catch (e) {
console.error('Failed to fetch data:', e);
Expand Down
25 changes: 21 additions & 4 deletions packages/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import * as commonSdk from '@powersync/common';
import { renderHook, waitFor } from '@testing-library/react';
import { vi, describe, expect, it, afterEach } from 'vitest';
import { useQuery } from '../src/hooks/useQuery';
import React from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PowerSyncContext } from '../src/hooks/PowerSyncContext';
import * as commonSdk from '@powersync/common';
import { useQuery } from '../src/hooks/useQuery';

const mockPowerSync = {
currentStatus: { status: 'initial' },
Expand Down Expand Up @@ -156,6 +156,23 @@ describe('useQuery', () => {
expect(currentResult.isLoading).toEqual(true);
});

it('should execute compatible queries', async () => {
const wrapper = ({ children }) => (
<PowerSyncContext.Provider value={mockPowerSync as any}>{children}</PowerSyncContext.Provider>
);

const query = () =>
useQuery({
execute: () => [{ test: 'custom' }] as any,
compile: () => ({ sql: 'SELECT * from lists', parameters: [] })
});
const { result } = renderHook(query, { wrapper });

await vi.waitFor(() => {
expect(result.current.data[0]?.test).toEqual('custom');
});
});

// The test returns unhandled errors when run with all the others.
// TODO: Fix the test so that there are no unhandled errors (this may be a vitest or @testing-library/react issue)
it.skip('should show an error if parsing the query results in an error', async () => {
Expand Down
13 changes: 8 additions & 5 deletions packages/vue/src/composables/useQuery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type SQLWatchOptions, parseQuery, type CompilableQuery, ParsedQuery } from '@powersync/common';
import { type CompilableQuery, ParsedQuery, type SQLWatchOptions, parseQuery } from '@powersync/common';
import { type MaybeRef, type Ref, ref, toValue, watchEffect } from 'vue';
import { usePowerSync } from './powerSync';

Expand Down Expand Up @@ -87,10 +87,10 @@ export const useQuery = <T = any>(
error.value = wrappedError;
};

const _fetchData = async (sql: string, parameters: any[]) => {
const _fetchData = async (executor: () => Promise<T[]>) => {
isFetching.value = true;
try {
const result = await powerSync.value.getAll<T>(sql, parameters);
const result = await executor();
handleResult(result);
} catch (e) {
console.error('Failed to fetch data:', e);
Expand All @@ -104,8 +104,9 @@ export const useQuery = <T = any>(
onCleanup(() => abortController.abort());

let parsedQuery: ParsedQuery;
const queryValue = toValue(query);
try {
parsedQuery = parseQuery(toValue(query), toValue(sqlParameters).map(toValue));
parsedQuery = parseQuery(queryValue, toValue(sqlParameters).map(toValue));
} catch (e) {
console.error('Failed to parse query:', e);
handleError(e);
Expand All @@ -123,7 +124,9 @@ export const useQuery = <T = any>(
return;
}
// Fetch initial data
fetchData = () => _fetchData(sql, parameters);
const executor =
typeof queryValue == 'string' ? () => powerSync.value.getAll<T>(sql, parameters) : () => queryValue.execute();
fetchData = () => _fetchData(executor);
await fetchData();

if (options.runQueryOnce) {
Expand Down
16 changes: 16 additions & 0 deletions packages/vue/tests/useQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,22 @@ describe('useQuery', () => {
expect(isLoading.value).toEqual(false);
});

it('should execute compilable queries', async () => {
vi.spyOn(PowerSync, 'usePowerSync').mockReturnValue(ref(mockPowerSync) as any);

const [{ isLoading, data }] = withSetup(() =>
useQuery({
execute: () => [{ test: 'custom' }] as any,
compile: () => ({ sql: 'SELECT * from lists', parameters: [] })
})
);

expect(isLoading.value).toEqual(true);
await flushPromises();
expect(isLoading.value).toEqual(false);
expect(data.value[0].test).toEqual('custom');
});

it('should set error for compilable query on useQuery parameters', async () => {
vi.spyOn(PowerSync, 'usePowerSync').mockReturnValue(ref(mockPowerSync) as any);

Expand Down
Loading