Skip to content

Commit 6036652

Browse files
update vue unit tests to use an actual DB
1 parent 9832417 commit 6036652

File tree

11 files changed

+426
-223
lines changed

11 files changed

+426
-223
lines changed

packages/common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
884884
return this.watchWithAsyncGenerator(sql, parameters, options);
885885
}
886886

887-
// TODO names and types
887+
// TODO names
888888
incrementalWatch<DataType>(options: IncrementalWatchOptions<DataType>): WatchedQuery<DataType> {
889889
const { watch, processor } = options;
890890

packages/react/tests/useQuery.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest';
66
import { PowerSyncContext } from '../src/hooks/PowerSyncContext';
77
import { useQuery } from '../src/hooks/watched/useQuery';
88
import { useWatchedQuerySubscription } from '../src/hooks/watched/useWatchedQuerySubscription';
9+
910
export const openPowerSync = () => {
1011
const db = new PowerSyncDatabase({
1112
database: { dbFilename: 'test.db' },

packages/vue/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
},
3535
"devDependencies": {
3636
"@powersync/common": "workspace:*",
37+
"@powersync/web": "workspace:*",
3738
"flush-promises": "^1.0.2",
3839
"jsdom": "^24.0.0",
3940
"vue": "3.4.21"
Lines changed: 10 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1-
import {
2-
type CompilableQuery,
3-
ParsedQuery,
4-
type SQLWatchOptions,
5-
parseQuery,
6-
runOnSchemaChange
7-
} from '@powersync/common';
8-
import { type MaybeRef, type Ref, ref, toValue, watchEffect } from 'vue';
9-
import { usePowerSync } from './powerSync';
10-
11-
interface AdditionalOptions extends Omit<SQLWatchOptions, 'signal'> {
12-
runQueryOnce?: boolean;
13-
}
1+
import { type CompilableQuery } from '@powersync/common';
2+
import { type MaybeRef, type Ref } from 'vue';
3+
import { AdditionalOptions, useSingleQuery } from './useSingleQuery';
4+
import { useWatchedQuery } from './useWatchedQuery';
145

156
export type WatchedQueryResult<T> = {
167
data: Ref<T[]>;
@@ -54,115 +45,12 @@ export type WatchedQueryResult<T> = {
5445
export const useQuery = <T = any>(
5546
query: MaybeRef<string | CompilableQuery<T>>,
5647
sqlParameters: MaybeRef<any[]> = [],
57-
options: AdditionalOptions = {}
48+
options: AdditionalOptions<T> = {}
5849
): WatchedQueryResult<T> => {
59-
const data = ref<T[]>([]) as Ref<T[]>;
60-
const error = ref<Error | undefined>(undefined);
61-
const isLoading = ref(true);
62-
const isFetching = ref(true);
63-
64-
// Only defined when the query and parameters are successfully parsed and tables are resolved
65-
let fetchData: () => Promise<void> | undefined;
66-
67-
const powerSync = usePowerSync();
68-
const logger = powerSync?.value?.logger ?? console;
69-
70-
const finishLoading = () => {
71-
isLoading.value = false;
72-
isFetching.value = false;
73-
};
74-
75-
if (!powerSync) {
76-
finishLoading();
77-
error.value = new Error('PowerSync not configured.');
78-
return { data, isLoading, isFetching, error };
50+
switch (true) {
51+
case options.runQueryOnce:
52+
return useSingleQuery(query, sqlParameters, options);
53+
default:
54+
return useWatchedQuery(query, sqlParameters, options);
7955
}
80-
81-
const handleResult = (result: T[]) => {
82-
finishLoading();
83-
data.value = result;
84-
error.value = undefined;
85-
};
86-
87-
const handleError = (e: Error) => {
88-
fetchData = undefined;
89-
finishLoading();
90-
data.value = [];
91-
92-
const wrappedError = new Error('PowerSync failed to fetch data: ' + e.message);
93-
wrappedError.cause = e;
94-
error.value = wrappedError;
95-
};
96-
97-
const _fetchData = async (executor: () => Promise<T[]>) => {
98-
isFetching.value = true;
99-
try {
100-
const result = await executor();
101-
handleResult(result);
102-
} catch (e) {
103-
logger.error('Failed to fetch data:', e);
104-
handleError(e);
105-
}
106-
};
107-
108-
watchEffect(async (onCleanup) => {
109-
const abortController = new AbortController();
110-
// Abort any previous watches when the effect triggers again, or when the component is unmounted
111-
onCleanup(() => abortController.abort());
112-
113-
let parsedQuery: ParsedQuery;
114-
const queryValue = toValue(query);
115-
try {
116-
parsedQuery = parseQuery(queryValue, toValue(sqlParameters).map(toValue));
117-
} catch (e) {
118-
logger.error('Failed to parse query:', e);
119-
handleError(e);
120-
return;
121-
}
122-
123-
const { sqlStatement: sql, parameters } = parsedQuery;
124-
const watchQuery = async (abortSignal: AbortSignal) => {
125-
let resolvedTables = [];
126-
try {
127-
resolvedTables = await powerSync.value.resolveTables(sql, parameters, options);
128-
} catch (e) {
129-
logger.error('Failed to fetch tables:', e);
130-
handleError(e);
131-
return;
132-
}
133-
// Fetch initial data
134-
const executor =
135-
typeof queryValue == 'string' ? () => powerSync.value.getAll<T>(sql, parameters) : () => queryValue.execute();
136-
fetchData = () => _fetchData(executor);
137-
await fetchData();
138-
139-
if (options.runQueryOnce) {
140-
return;
141-
}
142-
143-
powerSync.value.onChangeWithCallback(
144-
{
145-
onChange: async () => {
146-
await fetchData();
147-
},
148-
onError: handleError
149-
},
150-
{
151-
...options,
152-
signal: abortSignal,
153-
tables: resolvedTables
154-
}
155-
);
156-
};
157-
158-
runOnSchemaChange(watchQuery, powerSync.value, { signal: abortController.signal });
159-
});
160-
161-
return {
162-
data,
163-
isLoading,
164-
isFetching,
165-
error,
166-
refresh: () => fetchData?.()
167-
};
16856
};
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
type CompilableQuery,
3+
ParsedQuery,
4+
type SQLWatchOptions,
5+
WatchProcessorOptions,
6+
parseQuery
7+
} from '@powersync/common';
8+
import { type MaybeRef, type Ref, ref, toValue, watchEffect } from 'vue';
9+
import { usePowerSync } from './powerSync';
10+
11+
export interface AdditionalOptions<RowType = unknown> extends Omit<SQLWatchOptions, 'signal' | 'processor'> {
12+
runQueryOnce?: boolean;
13+
processor?: WatchProcessorOptions<RowType[]>;
14+
}
15+
16+
export type WatchedQueryResult<T> = {
17+
data: Ref<T[]>;
18+
/**
19+
* Indicates the initial loading state (hard loading). Loading becomes false once the first set of results from the watched query is available or an error occurs.
20+
*/
21+
isLoading: Ref<boolean>;
22+
/**
23+
* Indicates whether the query is currently fetching data, is true during the initial load and any time when the query is re-evaluating (useful for large queries).
24+
*/
25+
isFetching: Ref<boolean>;
26+
error: Ref<Error | undefined>;
27+
/**
28+
* Function used to run the query again.
29+
*/
30+
refresh?: () => Promise<void>;
31+
};
32+
33+
export const useSingleQuery = <T = any>(
34+
query: MaybeRef<string | CompilableQuery<T>>,
35+
sqlParameters: MaybeRef<any[]> = [],
36+
options: AdditionalOptions<T> = {}
37+
): WatchedQueryResult<T> => {
38+
const data = ref<T[]>([]) as Ref<T[]>;
39+
const error = ref<Error | undefined>(undefined);
40+
const isLoading = ref(true);
41+
const isFetching = ref(true);
42+
43+
// Only defined when the query and parameters are successfully parsed and tables are resolved
44+
let fetchData: () => Promise<void> | undefined;
45+
46+
const powerSync = usePowerSync();
47+
const logger = powerSync?.value?.logger ?? console;
48+
49+
const finishLoading = () => {
50+
isLoading.value = false;
51+
isFetching.value = false;
52+
};
53+
54+
if (!powerSync || !powerSync.value) {
55+
finishLoading();
56+
error.value = new Error('PowerSync not configured.');
57+
return { data, isLoading, isFetching, error };
58+
}
59+
60+
const handleResult = (result: T[]) => {
61+
finishLoading();
62+
data.value = result;
63+
error.value = undefined;
64+
};
65+
66+
const handleError = (e: Error) => {
67+
fetchData = undefined;
68+
finishLoading();
69+
data.value = [];
70+
71+
const wrappedError = new Error('PowerSync failed to fetch data: ' + e.message);
72+
wrappedError.cause = e;
73+
error.value = wrappedError;
74+
};
75+
76+
watchEffect(async (onCleanup) => {
77+
const abortController = new AbortController();
78+
// Abort any previous watches when the effect triggers again, or when the component is unmounted
79+
onCleanup(() => abortController.abort());
80+
81+
let parsedQuery: ParsedQuery;
82+
const queryValue = toValue(query);
83+
try {
84+
parsedQuery = parseQuery(queryValue, toValue(sqlParameters).map(toValue));
85+
} catch (e) {
86+
logger.error('Failed to parse query:', e);
87+
handleError(e);
88+
return;
89+
}
90+
91+
const { sqlStatement: sql, parameters } = parsedQuery;
92+
// Fetch initial data
93+
const executor =
94+
typeof queryValue == 'string' ? () => powerSync.value.getAll<T>(sql, parameters) : () => queryValue.execute();
95+
96+
fetchData = async () => {
97+
isFetching.value = true;
98+
try {
99+
const result = await executor();
100+
handleResult(result);
101+
} catch (e) {
102+
logger.error('Failed to fetch data:', e);
103+
handleError(e);
104+
}
105+
};
106+
107+
// fetch initial data
108+
await fetchData();
109+
});
110+
111+
return {
112+
data,
113+
isLoading,
114+
isFetching,
115+
error,
116+
refresh: () => fetchData?.()
117+
};
118+
};
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { type CompilableQuery, FalsyComparator, ParsedQuery, parseQuery } from '@powersync/common';
2+
import { type MaybeRef, type Ref, ref, toValue, watchEffect } from 'vue';
3+
import { usePowerSync } from './powerSync';
4+
import { AdditionalOptions, WatchedQueryResult } from './useSingleQuery';
5+
6+
export const useWatchedQuery = <T = any>(
7+
query: MaybeRef<string | CompilableQuery<T>>,
8+
sqlParameters: MaybeRef<any[]> = [],
9+
options: AdditionalOptions<T> = {}
10+
): WatchedQueryResult<T> => {
11+
const data = ref<T[]>([]) as Ref<T[]>;
12+
const error = ref<Error | undefined>(undefined);
13+
const isLoading = ref(true);
14+
const isFetching = ref(true);
15+
16+
const powerSync = usePowerSync();
17+
const logger = powerSync?.value?.logger ?? console;
18+
19+
const finishLoading = () => {
20+
isLoading.value = false;
21+
isFetching.value = false;
22+
};
23+
24+
if (!powerSync || !powerSync.value) {
25+
finishLoading();
26+
error.value = new Error('PowerSync not configured.');
27+
return { data, isLoading, isFetching, error };
28+
}
29+
30+
const handleError = (e: Error) => {
31+
finishLoading();
32+
data.value = [];
33+
34+
const wrappedError = new Error('PowerSync failed to fetch data: ' + e.message);
35+
wrappedError.cause = e;
36+
error.value = wrappedError;
37+
};
38+
39+
watchEffect(async (onCleanup) => {
40+
let parsedQuery: ParsedQuery;
41+
const queryValue = toValue(query);
42+
try {
43+
parsedQuery = parseQuery(queryValue, toValue(sqlParameters).map(toValue));
44+
} catch (e) {
45+
logger.error('Failed to parse query:', e);
46+
handleError(e);
47+
return;
48+
}
49+
50+
const { sqlStatement: sql, parameters } = parsedQuery;
51+
52+
const watchedQuery = powerSync.value.incrementalWatch({
53+
watch: {
54+
placeholderData: [],
55+
query: {
56+
compile: () => ({ sql, parameters }),
57+
execute: async ({ db, sql, parameters }) => {
58+
if (typeof queryValue === 'string') {
59+
return db.getAll<T>(sql, parameters);
60+
}
61+
return queryValue.execute();
62+
}
63+
}
64+
},
65+
processor: options.processor ?? {
66+
mode: 'comparison',
67+
// Defaults to no comparison if no processor is provided
68+
comparator: FalsyComparator
69+
}
70+
});
71+
72+
const disposer = watchedQuery.subscribe({
73+
onStateChange: (state) => {
74+
isLoading.value = state.isLoading;
75+
isFetching.value = state.isFetching;
76+
data.value = state.data;
77+
if (state.error) {
78+
const wrappedError = new Error('PowerSync failed to fetch data: ' + state.error.message);
79+
wrappedError.cause = state.error;
80+
error.value = wrappedError;
81+
} else {
82+
error.value = undefined;
83+
}
84+
}
85+
});
86+
87+
onCleanup(() => {
88+
disposer();
89+
watchedQuery.close();
90+
});
91+
});
92+
93+
return {
94+
data,
95+
isLoading,
96+
isFetching,
97+
error
98+
};
99+
};

0 commit comments

Comments
 (0)