Skip to content

feat(vue): update vue composables and compilable queries #153

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

Merged
merged 10 commits into from
May 6, 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/big-mugs-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@powersync/vue": minor
---

Introduced `useQuery` and `useStatus` composables which include compilable query support. Deprecated `usePowerSyncQuery`, `usePowerSyncWatchedQuery`, and `usePowerSyncStatus`.
5 changes: 5 additions & 0 deletions .changeset/seven-sheep-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vue-supabase-todolist": patch
---

Using new Vue `useQuery` and `useStatus` composables instead of deprecated ones.
14 changes: 7 additions & 7 deletions demos/vue-supabase-todolist/src/components/LoadingMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,27 @@
</template>

<script setup lang="ts">
import { usePowerSyncStatus } from '@powersync/vue';
import { useStatus } from '@powersync/vue';
import { ref } from 'vue';
import { watchEffect } from 'vue';
import { computed } from 'vue';

const { status } = usePowerSyncStatus();
const status = useStatus();

const props = defineProps({
loading: Boolean,
fetching: Boolean
isLoading: Boolean,
isFetching: Boolean
});

let dispose: (() => void) | undefined = undefined;
const showLoadingMessage = ref(false);

watchEffect(() => {
if (props.fetching || props.loading || !status.value.hasSynced) {
if (props.isFetching || props.isLoading || !status.value.hasSynced) {
if (!dispose) {
const timeout = setTimeout(() => {
showLoadingMessage.value = true;
}, 100);
}, 300);

dispose = () => clearTimeout(timeout);
}
Expand All @@ -46,5 +46,5 @@ watchEffect(() => {
}
});

const loadingMessage = computed(() => (props.loading ? 'Loading' : 'Re-executing query'));
const loadingMessage = computed(() => (props.isLoading ? 'Loading' : 'Re-executing query'));
</script>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="py-10 pa-2">
<LoadingMessage :loading :fetching />
<LoadingMessage v-if="isLoading || isFetching" :isLoading :isFetching />

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

<script setup lang="ts">
import { LISTS_TABLE, ListRecord, TODOS_TABLE } from '@/library/powersync/AppSchema';
import { TODO_LISTS_ROUTE } from '@/plugins/router'; // Adjust this import according to your project's structure
import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/vue'; // Adjust according to your actual implementation
import { TODO_LISTS_ROUTE } from '@/plugins/router';
import { usePowerSync, useQuery } from '@powersync/vue';
import { useRouter } from 'vue-router';
import ListItemWidget from './ListItemWidget.vue'; // Ensure this path is correct
import ListItemWidget from './ListItemWidget.vue';
import LoadingMessage from '../LoadingMessage.vue';

const powerSync = usePowerSync();
Expand All @@ -31,10 +31,10 @@ const router = useRouter();

const {
data: listRecords,
loading,
fetching,
isLoading,
isFetching,
error
} = usePowerSyncWatchedQuery<ListRecord & { total_tasks: number; completed_tasks: number }>(`
} = useQuery<ListRecord & { total_tasks: number; completed_tasks: number }>(`
SELECT
${LISTS_TABLE}.*, COUNT(${TODOS_TABLE}.id) AS total_tasks, SUM(CASE WHEN ${TODOS_TABLE}.completed = true THEN 1 ELSE 0 END) as completed_tasks
FROM
Expand Down
6 changes: 3 additions & 3 deletions demos/vue-supabase-todolist/src/views/SqlConsole.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<v-btn @click="executeQuery" class="ml-2" color="primary" height="auto">Execute Query</v-btn>
</div>
<div class="mt-9">
<LoadingMessage v-if="loading || fetching" :loading :fetching />
<LoadingMessage v-if="isLoading || isFetching" :isLoading :isFetching />
<ErrorMessage v-if="error">{{ error.message }}</ErrorMessage>
<v-container v-else-if="queryDataGridResult.columns.length > 0" class="mt-4 pa-0" fluid>
<v-data-table
Expand All @@ -21,13 +21,13 @@

<script setup lang="ts">
import { ref, computed } from 'vue';
import { usePowerSyncWatchedQuery } from '@powersync/vue';
import { useQuery } from '@powersync/vue';
const query = ref('SELECT * FROM lists');
const inputText = ref(query.value);
const executeQuery = () => {
query.value = inputText.value;
};
const { data: querySQLResult, loading, fetching, error } = usePowerSyncWatchedQuery(query);
const { data: querySQLResult, isLoading, isFetching, error } = useQuery(query);
const queryDataGridResult = computed(() => {
const firstItem = querySQLResult.value?.[0];
return {
Expand Down
19 changes: 7 additions & 12 deletions demos/vue-supabase-todolist/src/views/TodoListsEdit.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="py-10">
<LoadingMessage v-if="loading || fetching" :loading :fetching />
<LoadingMessage v-if="isLoading || isFetching" :isLoading :isFetching />
<v-list v-if="listName" class="pa-2 bg-black" lines="one">
<ErrorMessage v-if="error">{{ error.message }}</ErrorMessage>
<TodoItemWidget
Expand All @@ -13,7 +13,7 @@
@toggle-completion="toggleCompletion(record, !record.completed)"
/>
</v-list>
<h2 v-else-if="!loading" class="text-subtitle-1">No matching List found, please navigate back...</h2>
<h2 v-else-if="!isLoading" class="text-subtitle-1">No matching List found, please navigate back...</h2>

<v-dialog v-model="showPrompt" width="350" opacity="0.5" scrim="black">
<v-card title="Create Todo Item" class="bg-surface-light">
Expand Down Expand Up @@ -48,7 +48,7 @@ import TodoItemWidget from '@/components/widgets/TodoItemWidget.vue';
import { LISTS_TABLE, TODOS_TABLE, TodoRecord } from '@/library/powersync/AppSchema';
import { pageSubtitle } from '@/main';
import { supabase } from '@/plugins/supabase';
import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/vue';
import { usePowerSync, useQuery } from '@powersync/vue';
import { watch } from 'vue';
import { onUnmounted } from 'vue';
import { ref } from 'vue';
Expand All @@ -64,10 +64,7 @@ const setShowPrompt = (state: boolean): void => {
showPrompt.value = state;
};
const { id: listID = '' } = useRoute().params;
const { data: listRecords } = usePowerSyncWatchedQuery<{ name: string }>(
`SELECT name FROM ${LISTS_TABLE} WHERE id = ?`,
[listID]
);
const { data: listRecords } = useQuery<{ name: string }>(`SELECT name FROM ${LISTS_TABLE} WHERE id = ?`, [listID]);

const listName = computed(() => listRecords.value[0]?.name);
watch(listName, () => {
Expand All @@ -79,12 +76,10 @@ onUnmounted(() => {

const {
data: todoRecords,
loading,
fetching,
isLoading,
isFetching,
error
} = usePowerSyncWatchedQuery<TodoRecord>(`SELECT * FROM ${TODOS_TABLE} WHERE list_id=? ORDER BY created_at DESC, id`, [
listID
]);
} = useQuery<TodoRecord>(`SELECT * FROM ${TODOS_TABLE} WHERE list_id=? ORDER BY created_at DESC, id`, [listID]);

const toggleCompletion = async (record: TodoRecord, completed: boolean) => {
const updatedRecord = { ...record, completed: completed };
Expand Down
4 changes: 2 additions & 2 deletions demos/vue-supabase-todolist/src/views/layouts/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ import { supabase } from '@/plugins/supabase';
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { pageSubtitle } from '@/main';
import { usePowerSyncStatus } from '@powersync/vue';
import { useStatus } from '@powersync/vue';

const openDrawer = ref(false);
const { status: syncStatus } = usePowerSyncStatus();
const syncStatus = useStatus();
const router = useRouter();
const route = useRoute();

Expand Down
105 changes: 69 additions & 36 deletions packages/vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ app.use(powerSyncPlugin);
app.mount('#app');
```

### Overriding the PowerSync instance
## Overriding the PowerSync instance

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.

Expand All @@ -43,7 +43,7 @@ providePowerSync(db);
</script>
```

### Accessing PowerSync
## Accessing PowerSync

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

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

### Queries
## Query

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.
The `useQuery` composable provides a dynamic view of a given query. The data will automatically update when a dependent table is updated.

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.

```Vue
// TodoListDisplayQuery.vue
<script setup>
import { usePowerSyncQuery } from '@powersync/vue';
import { usePowerSync, useQuery } from '@powersync/vue';
import { ref } from 'vue';

const query = ref('SELECT * from lists');
const { data: list, error, loading, refresh} = usePowerSyncQuery(query);
const { data:list, isLoading, isFetching, error} = useQuery(query);

const powersync = usePowerSync();
const addList = () => {
powersync.value.execute('INSERT INTO lists (id, name) VALUES (?, ?)', [Math.round(Math.random() * 1000), 'list name']);
}
</script>

<template>
<input v-model="query" />
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<div v-if="isLoading">Loading...</div>
<div v-else-if="isFetching">Updating results...</div>

<div v-if="error">{{ error }}</div>
<ul v-else>
<li v-for="l in list" :key="l.id">{{ l.name }}</li>
</ul>
<button @click="refresh">Refresh</button>
<button @click="addList">Add list</button>
</template>
```

### Watched Queries
### Static query

The `usePowerSyncWatchedQuery` composable provides a dynamic view of a given query. The data will automatically update when a dependent table is updated.

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.
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.

```Vue
// TodoListDisplayWatchedQuery.vue
// TodoListDisplayStaticQuery.vue
<script setup>
import { usePowerSync, usePowerSyncWatchedQuery } from '@powersync/vue';
import { ref } from 'vue';
import { useQuery } from '@powersync/vue';

const query = ref('SELECT * from lists');
const { data: list, loading, fetching, error} = usePowerSyncWatchedQuery(query);

const powersync = usePowerSync();
const addList = () => {
powersync.value.execute('INSERT INTO lists (id, name) VALUES (?, ?)', [Math.round(Math.random() * 1000), 'list name']);
}
const { data: list, refresh } = useQuery('SELECT id, name FROM lists', [], {
runQueryOnce: true
});
</script>

<template>
<input v-model="query" />
<div v-if="loading">Loading...</div>
<div v-else-if="fetching">Updating results...</div>
<ul>
<li v-for="l in list" :key="l.name">{{ l.name }} + {{ l.id }}</li>
</ul>
<button @click="refresh">Refresh list</button>
</template>

<div v-if="error">{{ error }}</div>
<ul v-else>
<li v-for="l in list" :key="l.id">{{ l.name }}</li>
</ul>
<button @click="addList">Add list</button>
```

### TypeScript Support

A type can be specified for each row returned by `useQuery`. Remember to declare `lang="ts"` when defining a `script setup` block.

```Vue
// TodoListDisplayStaticQueryTypeScript.vue
<script setup lang="ts">
import { useQuery } from '@powersync/vue';

const { data } = useQuery<{ id: string, name: string }>('SELECT id, name FROM lists');
</script>

<template>
<ul>
<li v-for="l in data" :key="l.id">{{ l.name }}</li>
</ul>
</template>
```

### Connection Status
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.

```Vue
// TodolistDisplayQueryKysely.vue
<script setup lang="ts">
import { usePowerSync, useQuery } from '@powersync/vue';
import { wrapPowerSyncWithKysely } from '@powersync/kysely-driver';
import { Database } from '@/library/powersync/AppSchema';

const powerSync = usePowerSync();
const db = wrapPowerSyncWithKysely<Database>(powerSync.value);

const { data } = useQuery(db.selectFrom('lists').selectAll().where('name', 'like', '%Shopping%'));
</script>
```

## Connection Status

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.
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.

```Vue
// ConnectionStatus.vue
<script setup>
import { usePowerSyncStatus } from '@powersync/vue';
import { useStatus } from '@powersync/vue';

const { status } = usePowerSyncStatus();
const status = useStatus();
</script>

<template>
Expand All @@ -150,7 +183,7 @@ const { status } = usePowerSyncStatus();

### Top-level setup block

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`.
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`.

Incorrect Usage Example:
Using PowerSync composables in a nested function of a component.
Expand Down
6 changes: 5 additions & 1 deletion packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"scripts": {
"build": "tsc -b",
"clean": "rm -rf lib tsconfig.tsbuildinfo",
"watch": "tsc -b -w"
"watch": "tsc -b -w",
"test": "vitest"
},
"repository": {
"type": "git",
Expand All @@ -33,7 +34,10 @@
"vue": "*"
},
"devDependencies": {
"flush-promises": "^1.0.2",
"jsdom": "^24.0.0",
"typescript": "^5.1.3",
"vitest": "^1.5.1",
"vue": "3.4.21"
}
}
Loading