diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f8b2d85..277ca710a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ +## [1.4.6-0](https://github.com/nuxt-community/supabase-module/compare/v1.4.5...v1.4.6-0) (2025-01-10) + + +### Features + +* Added useSupabaseQuery to simplify fetching ([4448ad3](https://github.com/nuxt-community/supabase-module/commit/4448ad306e2c5857e76c981e631e39f75e566043)) + ## [1.4.5](https://github.com/nuxt-community/supabase-module/compare/v1.4.4...v1.4.5) (2024-12-18) ## [1.4.4](https://github.com/nuxt-community/supabase-module/compare/v1.4.3...v1.4.4) (2024-12-10) diff --git a/docs/content/4.usage/composables/useSupabaseQuery.md b/docs/content/4.usage/composables/useSupabaseQuery.md new file mode 100644 index 000000000..753f92e25 --- /dev/null +++ b/docs/content/4.usage/composables/useSupabaseQuery.md @@ -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 + +``` +## 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 + +``` diff --git a/package.json b/package.json index 1cacd8073..58554694e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nuxtjs/supabase", - "version": "1.4.5", + "version": "1.4.6-0", "description": "Supabase module for Nuxt", "repository": { "type": "git", diff --git a/src/runtime/composables/useSupabaseQuery.ts b/src/runtime/composables/useSupabaseQuery.ts new file mode 100644 index 000000000..e6d75c48b --- /dev/null +++ b/src/runtime/composables/useSupabaseQuery.ts @@ -0,0 +1,181 @@ +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, Database as ImportedDB } from '#build/types/supabase-database' + +type _StrippedPostgrestFilterBuilder, Result, RelationName = unknown, Relationships = unknown> = Omit, Exclude, 'order' | 'range' | 'limit'>> + +type StrippedPostgrestFilterBuilder, Result, RelationName = unknown, Relationships = unknown > = { + [K in keyof _StrippedPostgrestFilterBuilder]: PostgrestFilterBuilder[K] +} + +type FilterFn = Record, Result = unknown, RelationName = unknown, Relationships = unknown> = (filter: StrippedPostgrestFilterBuilder) => StrippedPostgrestFilterBuilder +// export declare interface useSupabaseSelect<_db extends Record = ImportedDB> extends useSupabaseSelectSchema<_db['public']> { +// schema<_schema extends keyof _db>(schema: _schema): useSupabaseSelectSchema<_db[_schema]> +// } + +// interface useSupabaseSelectSchema< +// _schema extends GenericSchema, +// > { +// < +// _relation extends keyof (_schema['Views'] & _schema['Tables']) = keyof (_schema['Views'] & _schema['Tables']), +// _relationTableOrView extends GenericTable | GenericView = _relation extends keyof _schema['Tables'] ? _schema['Tables'][_relation] : _relation extends keyof _schema['Views'] ? _schema['Views'][_relation] : never, +// _query extends string = '*', +// _result = UnstableGetResult<_schema, _relationTableOrView['Row'], _relation, _relationTableOrView['Relationships'], _query>, +// _builder = StrippedPostgrestFilterBuilder<_schema, _relationTableOrView['Row'], _result, _relationTableOrView, _relationTableOrView['Relationships'] >, +// _count extends 'exact' | 'planned' | 'estimated' | undefined = undefined, +// _single extends boolean = false, +// _returning = useSupabaseSelectReturns<_single extends true ? _result : _result[]> & _count extends undefined ? never : { count: Ref } & _single extends true ? never : { loadMore(count: number): Promise }, +// >(relation: _relation, query: _query, options?: { +// filter?: (builder: _builder) => _builder +// count?: _count +// single?: _single +// }): _returning & Promise<_returning> +// } + +interface useSupabaseSelectReturns<_result> { + data: Ref<_result> + error: Ref + status: Ref +} + +export function useSupabaseSelect< + _db extends Record = ImportedDB, + _schema extends GenericSchema = _db['public'], + const _relation extends string = (string & keyof _schema['Views']) | (string & keyof _schema['Tables']), + _relationTableOrView extends GenericTable | GenericView = _relation extends keyof _schema['Tables'] ? _schema['Tables'][_relation] : _relation extends keyof _schema['Views'] ? _schema['Views'][_relation] : never, + _result = UnstableGetResult<_schema, _relationTableOrView['Row'], _relation, _relationTableOrView['Relationships'], '*'>, + _returning = useSupabaseSelectReturns<_result[]> & { loadMore(count: number): Promise }, +>(relation: _relation): _returning & Promise<_returning> + +export function useSupabaseSelect< + _db extends Record = ImportedDB, + const _schema extends GenericSchema = _db['public'], + const _relation extends string = (string & keyof _schema['Views']) | (string & keyof _schema['Tables']), + _relationTableOrView extends GenericTable | GenericView = _relation extends keyof _schema['Tables'] ? _schema['Tables'][_relation] : _relation extends keyof _schema['Views'] ? _schema['Views'][_relation] : never, + _query extends string = string, + _result = UnstableGetResult<_schema, _relationTableOrView['Row'], _relation, _relationTableOrView['Relationships'], _query>, + _returning = useSupabaseSelectReturns<_result[]> & { loadMore(count: number): Promise }, +>(relation: _relation, query: _query): _returning & Promise<_returning> + +export function useSupabaseSelect< + _db extends Record = ImportedDB, + const _schema extends GenericSchema = _db['public'], + const _relation extends string = (string & keyof _schema['Views']) | (string & keyof _schema['Tables']), + _relationTableOrView extends GenericTable | GenericView = _relation extends keyof _schema['Tables'] ? _schema['Tables'][_relation] : _relation extends keyof _schema['Views'] ? _schema['Views'][_relation] : never, + const _query extends string = string, + _result = UnstableGetResult<_schema, _relationTableOrView['Row'], _relation, _relationTableOrView['Relationships'], _query>, + _count extends 'exact' | 'planned' | 'estimated' | undefined = 'exact' | 'planned' | 'estimated' | undefined, + _single extends boolean = boolean, + _returning = (useSupabaseSelectReturns<_single extends true ? _result : _result[]>) & { count: _count extends undefined ? never : Ref, loadMore: _single extends true ? never : (count: number) => Promise } , +>(relation: _relation, query: _query, options?: { + filter?: FilterFn<_schema, _relationTableOrView['Row'], _result, _relationTableOrView, _relationTableOrView['Relationships'] > + count?: _count + single?: _single +}): _returning & Promise<_returning> + +export function useSupabaseSelect(relation: string, query: string = '*', { count, single, filter }: { filter?: FilterFn, count?: 'exact' | 'planned' | 'estimated', single?: boolean } = {}) { + return _interal({ count, single, relation, query, schema: 'public', filter }) +} + +export function useSupabaseSelectSchema< + _db extends Record = ImportedDB, + const _schema_name extends string = string & keyof _db, + const _schema extends GenericSchema = _db[_schema_name], +>(schema: _schema_name): typeof useSupabaseSelect<_db, _schema> { + return function useSupabaseSelect<_db, _schema>(relation: string, query: string = '*', { count, single, filter }: { filter?: FilterFn, count?: 'exact' | 'planned' | 'estimated', single?: boolean } = {}) { + return _interal({ count, single, relation, query, schema, filter }) + } +} + +function _interal< + _returning = { + data: Ref + error: Ref + status: Ref + count?: Ref + loadMore?: (() => Promise) + }, +>({ + schema, + relation, + filter, + query, + single, + count, +}: { + schema: string + relation: string + query: string + filter?: FilterFn + single?: boolean + count?: 'exact' | 'planned' | 'estimated' +}): Promise<_returning> & _returning { + const nuxtApp = useNuxtApp() + const client = useSupabaseClient() + + const asyncData = reactive({ status: 'idle', data: null, error: null }) + const returning = toRefs(asyncData) + + function makeRequest() { + let request = schema + ? client.schema(schema).from(relation).select(query, { count }) + : client.from(relation).select(query, { count }) + if (filter) request = filter(request) // as unknown as StrippedPostgrestFilterBuilder) as unknown as PostgrestTransformBuilder + if (single) request.single() + return request + } + + const req = makeRequest() + // @ts-expect-error Property 'url' is protected and only accessible within class 'PostgrestBuilder' and its subclasses. + const key = req.url.pathname + req.url.search + if (import.meta.browser) { + // Watch for changes + let reqInProgress: ReturnType + 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) { + asyncData.status = 'pending' + // @ts-expect-error Property 'url' is protected and only accessible within class 'PostgrestBuilder' 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(resolve => handleRequest(req).then(resolve)) + + if (import.meta.server) { + promise.finally(() => nuxtApp.payload.data[key] ??= returning) + nuxtApp.hook('app:created', async () => { + await promise + }) + } + + return Object.assign(promise, returning) +} + +// immediate?: AsyncDataOptions['immediate'] +// deep?: AsyncDataOptions['deep'] +// lazy?: AsyncDataOptions['lazy'] +// server?: AsyncDataOptions['server'] +// watch?: AsyncDataOptions['watch']