Skip to content

Commit a5550b2

Browse files
ChriztiaanDominicGBauerChristiaan Landman
authored
feat(vue): update vue composables and compilable queries (#153)
Co-authored-by: DominicGBauer <[email protected]> Co-authored-by: Christiaan Landman <[email protected]>
1 parent 3e92a9c commit a5550b2

22 files changed

+697
-116
lines changed

.changeset/big-mugs-admire.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@powersync/vue": minor
3+
---
4+
5+
Introduced `useQuery` and `useStatus` composables which include compilable query support. Deprecated `usePowerSyncQuery`, `usePowerSyncWatchedQuery`, and `usePowerSyncStatus`.

.changeset/seven-sheep-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"vue-supabase-todolist": patch
3+
---
4+
5+
Using new Vue `useQuery` and `useStatus` composables instead of deprecated ones.

demos/vue-supabase-todolist/src/components/LoadingMessage.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,27 @@
1414
</template>
1515

1616
<script setup lang="ts">
17-
import { usePowerSyncStatus } from '@powersync/vue';
17+
import { useStatus } from '@powersync/vue';
1818
import { ref } from 'vue';
1919
import { watchEffect } from 'vue';
2020
import { computed } from 'vue';
2121
22-
const { status } = usePowerSyncStatus();
22+
const status = useStatus();
2323
2424
const props = defineProps({
25-
loading: Boolean,
26-
fetching: Boolean
25+
isLoading: Boolean,
26+
isFetching: Boolean
2727
});
2828
2929
let dispose: (() => void) | undefined = undefined;
3030
const showLoadingMessage = ref(false);
3131
3232
watchEffect(() => {
33-
if (props.fetching || props.loading || !status.value.hasSynced) {
33+
if (props.isFetching || props.isLoading || !status.value.hasSynced) {
3434
if (!dispose) {
3535
const timeout = setTimeout(() => {
3636
showLoadingMessage.value = true;
37-
}, 100);
37+
}, 300);
3838
3939
dispose = () => clearTimeout(timeout);
4040
}
@@ -46,5 +46,5 @@ watchEffect(() => {
4646
}
4747
});
4848
49-
const loadingMessage = computed(() => (props.loading ? 'Loading' : 'Re-executing query'));
49+
const loadingMessage = computed(() => (props.isLoading ? 'Loading' : 'Re-executing query'));
5050
</script>

demos/vue-supabase-todolist/src/components/widgets/TodoListsWidget.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div class="py-10 pa-2">
3-
<LoadingMessage :loading :fetching />
3+
<LoadingMessage v-if="isLoading || isFetching" :isLoading :isFetching />
44

55
<ErrorMessage v-if="error">{{ error.message }}</ErrorMessage>
66
<v-list v-else class="bg-black pa-0" lines="two">
@@ -18,10 +18,10 @@
1818

1919
<script setup lang="ts">
2020
import { LISTS_TABLE, ListRecord, TODOS_TABLE } from '@/library/powersync/AppSchema';
21-
import { TODO_LISTS_ROUTE } from '@/plugins/router'; // Adjust this import according to your project's structure
22-
import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/vue'; // Adjust according to your actual implementation
21+
import { TODO_LISTS_ROUTE } from '@/plugins/router';
22+
import { usePowerSync, useQuery } from '@powersync/vue';
2323
import { useRouter } from 'vue-router';
24-
import ListItemWidget from './ListItemWidget.vue'; // Ensure this path is correct
24+
import ListItemWidget from './ListItemWidget.vue';
2525
import LoadingMessage from '../LoadingMessage.vue';
2626
2727
const powerSync = usePowerSync();
@@ -31,10 +31,10 @@ const router = useRouter();
3131
3232
const {
3333
data: listRecords,
34-
loading,
35-
fetching,
34+
isLoading,
35+
isFetching,
3636
error
37-
} = usePowerSyncWatchedQuery<ListRecord & { total_tasks: number; completed_tasks: number }>(`
37+
} = useQuery<ListRecord & { total_tasks: number; completed_tasks: number }>(`
3838
SELECT
3939
${LISTS_TABLE}.*, COUNT(${TODOS_TABLE}.id) AS total_tasks, SUM(CASE WHEN ${TODOS_TABLE}.completed = true THEN 1 ELSE 0 END) as completed_tasks
4040
FROM

demos/vue-supabase-todolist/src/views/SqlConsole.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<v-btn @click="executeQuery" class="ml-2" color="primary" height="auto">Execute Query</v-btn>
66
</div>
77
<div class="mt-9">
8-
<LoadingMessage v-if="loading || fetching" :loading :fetching />
8+
<LoadingMessage v-if="isLoading || isFetching" :isLoading :isFetching />
99
<ErrorMessage v-if="error">{{ error.message }}</ErrorMessage>
1010
<v-container v-else-if="queryDataGridResult.columns.length > 0" class="mt-4 pa-0" fluid>
1111
<v-data-table
@@ -21,13 +21,13 @@
2121

2222
<script setup lang="ts">
2323
import { ref, computed } from 'vue';
24-
import { usePowerSyncWatchedQuery } from '@powersync/vue';
24+
import { useQuery } from '@powersync/vue';
2525
const query = ref('SELECT * FROM lists');
2626
const inputText = ref(query.value);
2727
const executeQuery = () => {
2828
query.value = inputText.value;
2929
};
30-
const { data: querySQLResult, loading, fetching, error } = usePowerSyncWatchedQuery(query);
30+
const { data: querySQLResult, isLoading, isFetching, error } = useQuery(query);
3131
const queryDataGridResult = computed(() => {
3232
const firstItem = querySQLResult.value?.[0];
3333
return {

demos/vue-supabase-todolist/src/views/TodoListsEdit.vue

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div class="py-10">
3-
<LoadingMessage v-if="loading || fetching" :loading :fetching />
3+
<LoadingMessage v-if="isLoading || isFetching" :isLoading :isFetching />
44
<v-list v-if="listName" class="pa-2 bg-black" lines="one">
55
<ErrorMessage v-if="error">{{ error.message }}</ErrorMessage>
66
<TodoItemWidget
@@ -13,7 +13,7 @@
1313
@toggle-completion="toggleCompletion(record, !record.completed)"
1414
/>
1515
</v-list>
16-
<h2 v-else-if="!loading" class="text-subtitle-1">No matching List found, please navigate back...</h2>
16+
<h2 v-else-if="!isLoading" class="text-subtitle-1">No matching List found, please navigate back...</h2>
1717

1818
<v-dialog v-model="showPrompt" width="350" opacity="0.5" scrim="black">
1919
<v-card title="Create Todo Item" class="bg-surface-light">
@@ -48,7 +48,7 @@ import TodoItemWidget from '@/components/widgets/TodoItemWidget.vue';
4848
import { LISTS_TABLE, TODOS_TABLE, TodoRecord } from '@/library/powersync/AppSchema';
4949
import { pageSubtitle } from '@/main';
5050
import { supabase } from '@/plugins/supabase';
51-
import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/vue';
51+
import { usePowerSync, useQuery } from '@powersync/vue';
5252
import { watch } from 'vue';
5353
import { onUnmounted } from 'vue';
5454
import { ref } from 'vue';
@@ -64,10 +64,7 @@ const setShowPrompt = (state: boolean): void => {
6464
showPrompt.value = state;
6565
};
6666
const { id: listID = '' } = useRoute().params;
67-
const { data: listRecords } = usePowerSyncWatchedQuery<{ name: string }>(
68-
`SELECT name FROM ${LISTS_TABLE} WHERE id = ?`,
69-
[listID]
70-
);
67+
const { data: listRecords } = useQuery<{ name: string }>(`SELECT name FROM ${LISTS_TABLE} WHERE id = ?`, [listID]);
7168
7269
const listName = computed(() => listRecords.value[0]?.name);
7370
watch(listName, () => {
@@ -79,12 +76,10 @@ onUnmounted(() => {
7976
8077
const {
8178
data: todoRecords,
82-
loading,
83-
fetching,
79+
isLoading,
80+
isFetching,
8481
error
85-
} = usePowerSyncWatchedQuery<TodoRecord>(`SELECT * FROM ${TODOS_TABLE} WHERE list_id=? ORDER BY created_at DESC, id`, [
86-
listID
87-
]);
82+
} = useQuery<TodoRecord>(`SELECT * FROM ${TODOS_TABLE} WHERE list_id=? ORDER BY created_at DESC, id`, [listID]);
8883
8984
const toggleCompletion = async (record: TodoRecord, completed: boolean) => {
9085
const updatedRecord = { ...record, completed: completed };

demos/vue-supabase-todolist/src/views/layouts/Layout.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ import { supabase } from '@/plugins/supabase';
4141
import { computed, ref } from 'vue';
4242
import { useRoute, useRouter } from 'vue-router';
4343
import { pageSubtitle } from '@/main';
44-
import { usePowerSyncStatus } from '@powersync/vue';
44+
import { useStatus } from '@powersync/vue';
4545
4646
const openDrawer = ref(false);
47-
const { status: syncStatus } = usePowerSyncStatus();
47+
const syncStatus = useStatus();
4848
const router = useRouter();
4949
const route = useRoute();
5050

packages/vue/README.md

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ app.use(powerSyncPlugin);
2323
app.mount('#app');
2424
```
2525

26-
### Overriding the PowerSync instance
26+
## Overriding the PowerSync instance
2727

2828
The `createPowerSyncPlugin` function is designed for setting up a PowerSync client that is available across your entire Vue application. It's the recommended approach for package setup. However, there may be situations where an app-wide setup isn't suitable, or you need a different PowerSync client for specific parts of your application.
2929

@@ -43,7 +43,7 @@ providePowerSync(db);
4343
</script>
4444
```
4545

46-
### Accessing PowerSync
46+
## Accessing PowerSync
4747

4848
The provided PowerSync client is available with the `usePowerSync` composable.
4949

@@ -65,75 +65,108 @@ powersync.value.getAll('SELECT * from lists').then((l) => list.value = l);
6565
</template>
6666
```
6767

68-
### Queries
68+
## Query
6969

70-
The `usePowerSyncQuery` composable provides a static view of a given query. You can use refs as parameters instead to automatically refresh the query when they change. The composable exposes reactive variables for the results, the loading state and error state, as well as a refresh callback that can be invoked to rerun the query manually.
70+
The `useQuery` composable provides a dynamic view of a given query. The data will automatically update when a dependent table is updated.
71+
72+
You can use refs as parameters to refresh the query when they change. The composable exposes reactive variables for the results as well as the loading, fetching, and and error states. Note that `isLoading` indicates that the initial result is being retrieved and `isFetching` indicates the query is fetching data, which could be for the initial load or any time when the query is re-evaluating due to a change in a dependent table.
7173

7274
```Vue
7375
// TodoListDisplayQuery.vue
7476
<script setup>
75-
import { usePowerSyncQuery } from '@powersync/vue';
77+
import { usePowerSync, useQuery } from '@powersync/vue';
7678
import { ref } from 'vue';
7779
7880
const query = ref('SELECT * from lists');
79-
const { data: list, error, loading, refresh} = usePowerSyncQuery(query);
81+
const { data:list, isLoading, isFetching, error} = useQuery(query);
82+
83+
const powersync = usePowerSync();
84+
const addList = () => {
85+
powersync.value.execute('INSERT INTO lists (id, name) VALUES (?, ?)', [Math.round(Math.random() * 1000), 'list name']);
86+
}
8087
</script>
8188
8289
<template>
8390
<input v-model="query" />
84-
<div v-if="loading">Loading...</div>
85-
<div v-else-if="error">{{ error }}</div>
91+
<div v-if="isLoading">Loading...</div>
92+
<div v-else-if="isFetching">Updating results...</div>
93+
94+
<div v-if="error">{{ error }}</div>
8695
<ul v-else>
8796
<li v-for="l in list" :key="l.id">{{ l.name }}</li>
8897
</ul>
89-
<button @click="refresh">Refresh</button>
98+
<button @click="addList">Add list</button>
9099
</template>
91100
```
92101

93-
### Watched Queries
102+
### Static query
94103

95-
The `usePowerSyncWatchedQuery` composable provides a dynamic view of a given query. The data will automatically update when a dependent table is updated.
96-
97-
You can use refs as parameters to refresh the query when they change. The composable exposes reactive variables for the results as well as the loading, fetching, and and error states. Note that `loading` initicates that the initial result is being retrieved and `fetching` indicates the query is fetching data, which could be for the initial load or any time when the query is re-evaluating due to a change in a dependent table.
104+
The `useQuery` composable can be configured to only execute initially and not every time changes to dependent tables are detected. The query can be manually re-executed by using the returned `refresh` function.
98105

99106
```Vue
100-
// TodoListDisplayWatchedQuery.vue
107+
// TodoListDisplayStaticQuery.vue
101108
<script setup>
102-
import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/vue';
103-
import { ref } from 'vue';
109+
import { useQuery } from '@powersync/vue';
104110
105-
const query = ref('SELECT * from lists');
106-
const { data: list, loading, fetching, error} = usePowerSyncWatchedQuery(query);
107-
108-
const powersync = usePowerSync();
109-
const addList = () => {
110-
powersync.value.execute('INSERT INTO lists (id, name) VALUES (?, ?)', [Math.round(Math.random() * 1000), 'list name']);
111-
}
111+
const { data: list, refresh } = useQuery('SELECT id, name FROM lists', [], {
112+
runQueryOnce: true
113+
});
112114
</script>
113115
114116
<template>
115-
<input v-model="query" />
116-
<div v-if="loading">Loading...</div>
117-
<div v-else-if="fetching">Updating results...</div>
117+
<ul>
118+
<li v-for="l in list" :key="l.name">{{ l.name }} + {{ l.id }}</li>
119+
</ul>
120+
<button @click="refresh">Refresh list</button>
121+
</template>
118122
119-
<div v-if="error">{{ error }}</div>
120-
<ul v-else>
121-
<li v-for="l in list" :key="l.id">{{ l.name }}</li>
122-
</ul>
123-
<button @click="addList">Add list</button>
123+
```
124+
125+
### TypeScript Support
126+
127+
A type can be specified for each row returned by `useQuery`. Remember to declare `lang="ts"` when defining a `script setup` block.
128+
129+
```Vue
130+
// TodoListDisplayStaticQueryTypeScript.vue
131+
<script setup lang="ts">
132+
import { useQuery } from '@powersync/vue';
133+
134+
const { data } = useQuery<{ id: string, name: string }>('SELECT id, name FROM lists');
135+
</script>
136+
137+
<template>
138+
<ul>
139+
<li v-for="l in data" :key="l.id">{{ l.name }}</li>
140+
</ul>
124141
</template>
125142
```
126143

127-
### Connection Status
144+
You are also able to use a compilable query (e.g. [Kysely queries](https://github.com/powersync-ja/powersync-js/tree/main/packages/kysely-driver)) as a query argument in place of a SQL statement string.
145+
146+
```Vue
147+
// TodolistDisplayQueryKysely.vue
148+
<script setup lang="ts">
149+
import { usePowerSync, useQuery } from '@powersync/vue';
150+
import { wrapPowerSyncWithKysely } from '@powersync/kysely-driver';
151+
import { Database } from '@/library/powersync/AppSchema';
152+
153+
const powerSync = usePowerSync();
154+
const db = wrapPowerSyncWithKysely<Database>(powerSync.value);
155+
156+
const { data } = useQuery(db.selectFrom('lists').selectAll().where('name', 'like', '%Shopping%'));
157+
</script>
158+
```
159+
160+
## Connection Status
128161

129-
The `usePowerSyncStatus` composable provides general connectivity information such as the connection status, whether the initial full sync has completed, when the last sync completed, and whether any data is being uploaded or downloaded.
162+
The `useStatus` composable provides general connectivity information such as the connection status, whether the initial full sync has completed, when the last sync completed, and whether any data is being uploaded or downloaded.
130163

131164
```Vue
132165
// ConnectionStatus.vue
133166
<script setup>
134-
import { usePowerSyncStatus } from '@powersync/vue';
167+
import { useStatus } from '@powersync/vue';
135168
136-
const { status } = usePowerSyncStatus();
169+
const status = useStatus();
137170
</script>
138171
139172
<template>
@@ -150,7 +183,7 @@ const { status } = usePowerSyncStatus();
150183

151184
### Top-level setup block
152185

153-
The `usePowersync`, `usePowerSyncQuery`, `usePowerSyncWatchedQuery`, and `usePowerSyncStatus` composables are meant to be invoked in the top-level setup block. Vue expects certain Composition API functions, like `inject` which this package depends on, to be resolved in the setup context and not inside nested or asynchronous functions. For use cases where you need to do this, you should access the PowerSync `AbstractPowerSyncDatabase` instance directly - like exporting it as singleton after configuring Vue with it in `main.js`.
186+
The `usePowersync`, `useQuery`, and `useStatus` composables are meant to be invoked in the top-level setup block. Vue expects certain Composition API functions, like `inject` which this package depends on, to be resolved in the setup context and not inside nested or asynchronous functions. For use cases where you need to do this, you should access the PowerSync `AbstractPowerSyncDatabase` instance directly - like exporting it as singleton after configuring Vue with it in `main.js`.
154187

155188
Incorrect Usage Example:
156189
Using PowerSync composables in a nested function of a component.

packages/vue/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"scripts": {
1515
"build": "tsc -b",
1616
"clean": "rm -rf lib tsconfig.tsbuildinfo",
17-
"watch": "tsc -b -w"
17+
"watch": "tsc -b -w",
18+
"test": "vitest"
1819
},
1920
"repository": {
2021
"type": "git",
@@ -33,7 +34,10 @@
3334
"vue": "*"
3435
},
3536
"devDependencies": {
37+
"flush-promises": "^1.0.2",
38+
"jsdom": "^24.0.0",
3639
"typescript": "^5.1.3",
40+
"vitest": "^1.5.1",
3741
"vue": "3.4.21"
3842
}
3943
}

0 commit comments

Comments
 (0)