Skip to content

Commit

Permalink
feat: Added useSupabaseQuery to simplify fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-hale committed Jan 10, 2025
1 parent 3783606 commit 794729b
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 0 deletions.
41 changes: 41 additions & 0 deletions docs/content/4.usage/composables/useSupabaseQuery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: useSupabaseQuery
description: Abstraction around the Supabase select API similar to useAsyncData
---

This composable is using [postgres-js](https://github.com/supabase/postgrest-js) under the hood.

## Usage

The useSupabaseQuery composable is wrapping the methods and return types of `useSupabaseClient().schema('...').from('...').select('...')`. For a more convinient developer experience similar to using useAsyncData.

```vue
<script setup lang="ts">
const { data, count, error, status, loadMore } = await useSupabaseQuery(
'objects',
'*',
filter => filter.eq('id', 'example'),
{count: 'exact', single: false, limit: 10, schema: 'storage'}
)
</script>
```
## Options
### Count
When using `{count: 'exact' | 'estimated' | 'planned'}` an additional count ref will be returned with a number.
### Single
When `single: true` .single() is automatically called, and data is properly typed for a single result instead of an Array
### Limit
When specifying a `limit`, an additional loadMore object is returned which will make an additional request using `.range(data.value.length, limit)`, and push it to `data.value`
### Schema
A schema can be provied, although `public` is already selected as the default.
## Typescript

Database typings are handled automatically.

You can also pass Database typings and Schema manually:

```vue
<script setup lang="ts">
const { data } = await useSupabaseQuery<Database>('table', '*', filter => filter.eq('id', 'example'), {count: 'exact', single: true})
</script>
```
119 changes: 119 additions & 0 deletions src/runtime/composables/useSupabaseQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { PostgrestError, PostgrestFilterBuilder, PostgrestTransformBuilder, UnstableGetResult } from '@supabase/postgrest-js'
import type { GenericSchema, GenericTable, GenericView } from '@supabase/supabase-js/dist/module/lib/types'
import type { AsyncDataRequestStatus } from 'nuxt/app'
import { reactive, toRefs, watchEffect, type Ref } from 'vue'
import { useNuxtApp, useSupabaseClient } from '#imports'
import type { Database as ImportedDB } from '#build/types/supabase-database'

type _StrippedPostgrestFilterBuilder<Schema extends GenericSchema, Row extends Record<string, unknown>, Result, RelationName, Relationships > = Omit<PostgrestFilterBuilder< Schema, Row, Result, RelationName, Relationships>, Exclude<keyof PostgrestTransformBuilder<Schema, Row, Result, RelationName, Relationships>, 'order' | 'range' | 'limit'>>

type StrippedPostgrestFilterBuilder<Schema extends GenericSchema, Row extends Record<string, unknown>, Result, RelationName, Relationships > = {
[K in keyof _StrippedPostgrestFilterBuilder<Schema, Row, Result, RelationName, Relationships>]: PostgrestFilterBuilder<Schema, Row, Result, RelationName, Relationships>[K]
}

export function useSupabaseQuery<
const Database extends Record<string, GenericSchema> & ImportedDB,
_single extends boolean = false,
_count extends 'exact' | 'planned' | 'estimated' | undefined = undefined,
_limit extends number | undefined = undefined,

const SchemaName extends string & keyof Database = 'public',
const Schema extends GenericSchema = Database[SchemaName],
const RelationName extends string = string & (keyof Database[SchemaName]['Tables'] | keyof Database[SchemaName]['Views']),
const Relation extends GenericTable | GenericView = RelationName extends keyof Schema['Tables'] ? Schema['Tables'][RelationName] : RelationName extends keyof Schema['Views'] ? Schema['Views'][RelationName] : never,
const Query extends string = '*',
ResultOne = UnstableGetResult<Schema, Relation['Row'], RelationName, Relation['Relationships'], Query>,
_returning = {
data: Ref<_single extends true ? ResultOne : ResultOne[]>
error: Ref<PostgrestError>
status: Ref<AsyncDataRequestStatus>
count: _count extends undefined ? undefined : Ref<number>
loadMore: _limit extends undefined ? undefined : (() => Promise<void>)
},
>(
relation: RelationName,
query: Query,
filter: (builder: StrippedPostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], RelationName, Relation['Relationships']>) => StrippedPostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], RelationName, Relation['Relationships']>,
{ single, count, limit, schema }: {
single?: _single
count?: _count
limit?: _limit
schema?: SchemaName
} = {},
): Promise<_returning> & _returning {
const nuxtApp = useNuxtApp()
const client = useSupabaseClient<Database>()

const asyncData = reactive({ status: 'idle', data: null, error: null })
const returning = toRefs(asyncData)

function makeRequest() {
const request = schema
? client.schema<SchemaName>(schema).from(relation).select(query, { count })
: client.from(relation).select(query, { count })
const filteredRequest = filter(request as unknown as StrippedPostgrestFilterBuilder<Schema, Relation['Row'], ResultOne[], RelationName, Relation['Relationships']>) as unknown as PostgrestTransformBuilder<Schema, Relation['Row'], ResultOne[], RelationName, Relation['Relationships']>
if (single) filteredRequest.single()
if (limit) filteredRequest.limit(limit)
return filteredRequest
}

const req = makeRequest()
// @ts-expect-error Property 'url' is protected and only accessible within class 'PostgrestBuilder<Result>' and its subclasses.
const key = req.url.pathname + req.url.search
if (import.meta.browser) {
// Watch for changes
let reqInProgress: ReturnType<typeof handleRequest>
watchEffect(async () => {
if (reqInProgress) await reqInProgress
reqInProgress = handleRequest(makeRequest())
})

// loadMore
if (limit && !single)
Object.assign(returning, {
async loadMore() {
if (!Array.isArray(asyncData.data)) throw new Error('asyncData.data is not an array, so more values cannot be loaded into it.')
asyncData.status = 'pending'
const { data, error, count } = await makeRequest().range(asyncData.data.length, asyncData.data.length + limit)
Object.assign(asyncData, { error, count })
asyncData.data.push(data)
asyncData.status = error ? 'error' : 'success'
},
})
}

async function handleRequest(request: ReturnType<typeof makeRequest>) {
asyncData.status = 'pending'
// @ts-expect-error Property 'url' is protected and only accessible within class 'PostgrestBuilder<Result>' and its subclasses.
const { data, error, count } = nuxtApp.payload.data[req.url.pathname + req.url.search] || await request
Object.assign(asyncData, { data, error, count })
asyncData.status = error ? 'error' : 'success'
return returning
}

const promise = new Promise<typeof returning>(resolve => handleRequest(req).then(resolve))

if (import.meta.server) {
promise.finally(() => nuxtApp.payload.data[key] ??= returning)
nuxtApp.hook('app:created', async () => { await promise })

Check failure on line 98 in src/runtime/composables/useSupabaseQuery.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 20)

This line has 2 statements. Maximum allowed is 1
}

return Object.assign(promise, returning)
}

export interface useSupabaseQueryOptions<
_single extends boolean = false,
_count extends 'exact' | 'planned' | 'estimated' | undefined = undefined,
_limit extends number | undefined = undefined,
_schema extends string = 'public',
> {
// immediate?: AsyncDataOptions<unknown>['immediate']
// deep?: AsyncDataOptions<unknown>['deep']
// lazy?: AsyncDataOptions<unknown>['lazy']
// server?: AsyncDataOptions<unknown>['server']
// watch?: AsyncDataOptions<unknown>['watch']
single?: _single
schema?: _schema
limit?: _limit
count?: _count
}

0 comments on commit 794729b

Please sign in to comment.