From 5666559caea6896e08b64bbdd038131e9272e6a4 Mon Sep 17 00:00:00 2001 From: Christian Pannwitz Date: Fri, 5 Apr 2024 10:09:48 +0200 Subject: [PATCH 1/5] feature: postgrest-vue-query package --- package.json | 2 +- packages/postgrest-vue-query/.eslintrc.json | 4 + packages/postgrest-vue-query/CHANGELOG.md | 3 + packages/postgrest-vue-query/README.md | 29 ++ .../__tests__/cache/use-mutate-item.spec.tsx | 90 ++++ .../__tests__/database.types.ts | 418 ++++++++++++++++ ...-delete-many-mutation.integration.spec.tsx | 133 +++++ .../use-delete-mutation.integration.spec.tsx | 217 ++++++++ .../use-insert-mutation.integration.spec.tsx | 79 +++ .../use-update-mutation.integration.spec.tsx | 87 ++++ .../use-upsert-mutation.integration.spec.tsx | 92 ++++ .../__tests__/query/fetch.spec.ts | 45 ++ .../query/prefetch.integration.spec.ts | 53 ++ .../query/use-query.integration.spec.tsx | 226 +++++++++ ...se-subscription-query-integration.spec.tsx | 112 +++++ .../use-subscription.integration.spec.tsx | 92 ++++ .../postgrest-vue-query/__tests__/utils.tsx | 21 + packages/postgrest-vue-query/package.json | 82 +++ .../postgrest-vue-query/prettier.config.cjs | 1 + .../postgrest-vue-query/src/cache/index.ts | 3 + .../src/cache/use-delete-item.ts | 40 ++ .../src/cache/use-mutate-item.ts | 41 ++ .../src/cache/use-upsert-item.ts | 40 ++ packages/postgrest-vue-query/src/index.ts | 10 + packages/postgrest-vue-query/src/lib/index.ts | 3 + packages/postgrest-vue-query/src/lib/key.ts | 71 +++ .../src/lib/use-postgrest-filter-cache.ts | 29 ++ .../src/lib/use-queries-for-table-loader.ts | 26 + .../src/mutate/get-user-response.ts | 14 + .../postgrest-vue-query/src/mutate/index.ts | 5 + .../postgrest-vue-query/src/mutate/types.ts | 91 ++++ .../src/mutate/use-delete-many-mutation.ts | 76 +++ .../src/mutate/use-delete-mutation.ts | 75 +++ .../src/mutate/use-insert-mutation.ts | 75 +++ .../src/mutate/use-update-mutation.ts | 70 +++ .../src/mutate/use-upsert-mutation.ts | 69 +++ .../src/query/build-query-opts.ts | 27 + .../postgrest-vue-query/src/query/fetch.ts | 51 ++ .../postgrest-vue-query/src/query/index.ts | 4 + .../postgrest-vue-query/src/query/prefetch.ts | 76 +++ .../src/query/use-query.ts | 142 ++++++ .../src/subscribe/index.ts | 2 + .../src/subscribe/use-subscription-query.ts | 172 +++++++ .../src/subscribe/use-subscription.ts | 98 ++++ packages/postgrest-vue-query/tsconfig.json | 5 + packages/postgrest-vue-query/tsup.config.ts | 15 + pnpm-lock.yaml | 473 +++++++++++++++++- 47 files changed, 3563 insertions(+), 26 deletions(-) create mode 100644 packages/postgrest-vue-query/.eslintrc.json create mode 100644 packages/postgrest-vue-query/CHANGELOG.md create mode 100644 packages/postgrest-vue-query/README.md create mode 100644 packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/database.types.ts create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/query/fetch.spec.ts create mode 100644 packages/postgrest-vue-query/__tests__/query/prefetch.integration.spec.ts create mode 100644 packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query-integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/utils.tsx create mode 100644 packages/postgrest-vue-query/package.json create mode 100644 packages/postgrest-vue-query/prettier.config.cjs create mode 100644 packages/postgrest-vue-query/src/cache/index.ts create mode 100644 packages/postgrest-vue-query/src/cache/use-delete-item.ts create mode 100644 packages/postgrest-vue-query/src/cache/use-mutate-item.ts create mode 100644 packages/postgrest-vue-query/src/cache/use-upsert-item.ts create mode 100644 packages/postgrest-vue-query/src/index.ts create mode 100644 packages/postgrest-vue-query/src/lib/index.ts create mode 100644 packages/postgrest-vue-query/src/lib/key.ts create mode 100644 packages/postgrest-vue-query/src/lib/use-postgrest-filter-cache.ts create mode 100644 packages/postgrest-vue-query/src/lib/use-queries-for-table-loader.ts create mode 100644 packages/postgrest-vue-query/src/mutate/get-user-response.ts create mode 100644 packages/postgrest-vue-query/src/mutate/index.ts create mode 100644 packages/postgrest-vue-query/src/mutate/types.ts create mode 100644 packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts create mode 100644 packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts create mode 100644 packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts create mode 100644 packages/postgrest-vue-query/src/mutate/use-update-mutation.ts create mode 100644 packages/postgrest-vue-query/src/mutate/use-upsert-mutation.ts create mode 100644 packages/postgrest-vue-query/src/query/build-query-opts.ts create mode 100644 packages/postgrest-vue-query/src/query/fetch.ts create mode 100644 packages/postgrest-vue-query/src/query/index.ts create mode 100644 packages/postgrest-vue-query/src/query/prefetch.ts create mode 100644 packages/postgrest-vue-query/src/query/use-query.ts create mode 100644 packages/postgrest-vue-query/src/subscribe/index.ts create mode 100644 packages/postgrest-vue-query/src/subscribe/use-subscription-query.ts create mode 100644 packages/postgrest-vue-query/src/subscribe/use-subscription.ts create mode 100644 packages/postgrest-vue-query/tsconfig.json create mode 100644 packages/postgrest-vue-query/tsup.config.ts diff --git a/package.json b/package.json index b6766938..b91a6a06 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,5 @@ "pnpm": "8", "node": ">=14.0.0" }, - "packageManager": "pnpm@8" + "packageManager": "pnpm@8.15.6" } diff --git a/packages/postgrest-vue-query/.eslintrc.json b/packages/postgrest-vue-query/.eslintrc.json new file mode 100644 index 00000000..c54a0612 --- /dev/null +++ b/packages/postgrest-vue-query/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": ["@supabase-cache-helpers/custom"] +} diff --git a/packages/postgrest-vue-query/CHANGELOG.md b/packages/postgrest-vue-query/CHANGELOG.md new file mode 100644 index 00000000..82b10ad3 --- /dev/null +++ b/packages/postgrest-vue-query/CHANGELOG.md @@ -0,0 +1,3 @@ +# @supabase-cache-helpers/postgrest-vue-query + +## 0.0.1 diff --git a/packages/postgrest-vue-query/README.md b/packages/postgrest-vue-query/README.md new file mode 100644 index 00000000..800b3e94 --- /dev/null +++ b/packages/postgrest-vue-query/README.md @@ -0,0 +1,29 @@ +# PostgREST React Query + +A collection of React Query utilities for working with Supabase. + +Latest build +GitHub Stars +[![codecov](https://codecov.io/gh/psteinroe/supabase-cache-helpers/branch/main/graph/badge.svg?token=SPMWSVBRGX)](https://codecov.io/gh/psteinroe/supabase-cache-helpers) + +## Introduction + +The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-react-query.vercel.app/) and find out how it feels like for your users. + +## Features + +With just one single line of code, you can simplify the logic of **fetching, subscribing to updates, and mutating data as well as storage objects** in your project, and have all the amazing features of [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest) out-of-the-box. + +- **Seamless** integration with [SWR](https://swr.vercel.app) and [React Query](https://tanstack.com/query/latest) +- **Automatic** cache key generation +- Easy **Pagination** and **Infinite Scroll** queries +- **Insert**, **update**, **upsert** and **delete** mutations +- **Auto-populate** cache after mutations and subscriptions +- **Auto-expand** mutation queries based on existing cache data to keep app up-to-date +- One-liner to upload, download and remove **Supabase Storage** objects + +And a lot [more](https://supabase-cache-helpers.vercel.app). + +--- + +**View full documentation and examples on [supabase-cache-helpers.vercel.app](https://supabase-cache-helpers.vercel.app).** diff --git a/packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.tsx b/packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.tsx new file mode 100644 index 00000000..d40d49d1 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.tsx @@ -0,0 +1,90 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; +import { fireEvent, screen } from '@testing-library/react'; +import React from 'react'; + +import { useMutateItem, useQuery } from '../../src'; +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-mutate-item'; + +describe('useMutateItem', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + let contacts: Database['public']['Tables']['contact']['Row'][]; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + }); + + beforeEach(async () => { + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + + const { data } = await client + .from('contact') + .insert( + new Array(3) + .fill(0) + .map((_, idx) => ({ username: `${testRunPrefix}-${idx}` })), + ) + .select('*'); + contacts = data as Database['public']['Tables']['contact']['Row'][]; + }); + + it('should mutate existing item in cache', async () => { + const queryClient = new QueryClient(); + function Page() { + const { data, count } = useQuery( + client + .from('contact') + .select('id,username', { count: 'exact' }) + .ilike('username', `${testRunPrefix}%`), + ); + + const mutate = useMutateItem({ + schema: 'public', + table: 'contact', + primaryKeys: ['id'], + }); + + return ( +
+
+ await mutate( + { + id: (data ?? []).find((c) => c)?.id, + }, + (c) => ({ ...c, username: `${c.username}-updated` }), + ) + } + /> + {(data ?? []).map((d) => ( + {d.username} + ))} + {`count: ${count}`} +
+ ); + } + + renderWithConfig(, queryClient); + await screen.findByText( + `count: ${contacts.length}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('mutate')); + await screen.findByText( + `${testRunPrefix}-0-updated`, + {}, + { timeout: 10000 }, + ); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/database.types.ts b/packages/postgrest-vue-query/__tests__/database.types.ts new file mode 100644 index 00000000..a1905a1b --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/database.types.ts @@ -0,0 +1,418 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[]; + +export interface Database { + public: { + Tables: { + address_book: { + Row: { + id: string; + created_at: string; + name: string | null; + }; + Insert: { + name?: string | null; + }; + Update: { + name?: string | null; + }; + Relationships: []; + }; + address_book_contact: { + Row: { + address_book: string; + contact: string; + }; + Insert: { + address_book: string; + contact: string; + }; + Update: { + address_book: string; + contact: string; + }; + Relationships: [ + { + foreignKeyName: 'address_book_contact_address_book_fkey'; + columns: ['address_book']; + referencedRelation: 'address_book'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'address_book_contact_contact_fkey'; + columns: ['contact']; + referencedRelation: 'contact'; + referencedColumns: ['id']; + }, + ]; + }; + contact: { + Row: { + age_range: unknown | null; + catchphrase: unknown | null; + continent: string | null; + country: string | null; + created_at: string; + golden_ticket: boolean | null; + id: string; + metadata: Json | null; + tags: string[] | null; + ticket_number: number | null; + username: string | null; + has_low_ticket_number: unknown | null; + }; + Insert: { + age_range?: unknown | null; + catchphrase?: unknown | null; + continent?: string | null; + country?: string | null; + created_at?: string; + golden_ticket?: boolean | null; + id?: string; + metadata?: Json | null; + tags?: string[] | null; + ticket_number?: number | null; + username?: string | null; + }; + Update: { + age_range?: unknown | null; + catchphrase?: unknown | null; + continent?: string | null; + country?: string | null; + created_at?: string; + golden_ticket?: boolean | null; + id?: string; + metadata?: Json | null; + tags?: string[] | null; + ticket_number?: number | null; + username?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'contact_continent_fkey'; + columns: ['continent']; + referencedRelation: 'continent'; + referencedColumns: ['code']; + }, + { + foreignKeyName: 'contact_country_fkey'; + columns: ['country']; + referencedRelation: 'country'; + referencedColumns: ['code']; + }, + ]; + }; + contact_note: { + Row: { + contact_id: string; + created_at: string; + id: string; + text: string; + }; + Insert: { + contact_id: string; + created_at?: string; + id?: string; + text: string; + }; + Update: { + contact_id?: string; + created_at?: string; + id?: string; + text?: string; + }; + Relationships: [ + { + foreignKeyName: 'contact_note_contact_id_fkey'; + columns: ['contact_id']; + referencedRelation: 'contact'; + referencedColumns: ['id']; + }, + ]; + }; + continent: { + Row: { + code: string; + name: string | null; + }; + Insert: { + code: string; + name?: string | null; + }; + Update: { + code?: string; + name?: string | null; + }; + Relationships: []; + }; + country: { + Row: { + code: string; + continent_code: string; + full_name: string; + iso3: string; + name: string; + number: string; + }; + Insert: { + code: string; + continent_code: string; + full_name: string; + iso3: string; + name: string; + number: string; + }; + Update: { + code?: string; + continent_code?: string; + full_name?: string; + iso3?: string; + name?: string; + number?: string; + }; + Relationships: [ + { + foreignKeyName: 'country_continent_code_fkey'; + columns: ['continent_code']; + referencedRelation: 'continent'; + referencedColumns: ['code']; + }, + ]; + }; + multi_pk: { + Row: { + id_1: number; + id_2: number; + name: string | null; + }; + Insert: { + id_1: number; + id_2: number; + name?: string | null; + }; + Update: { + id_1?: number; + id_2?: number; + name?: string | null; + }; + Relationships: []; + }; + serial_key_table: { + Row: { + id: number; + value: string | null; + }; + Insert: { + id?: number; + value?: string | null; + }; + Update: { + id?: number; + value?: string | null; + }; + Relationships: []; + }; + }; + Views: { + [_ in never]: never; + }; + Functions: { + has_low_ticket_number: { + Args: { + '': unknown; + }; + Returns: boolean; + }; + }; + Enums: { + [_ in never]: never; + }; + CompositeTypes: { + [_ in never]: never; + }; + }; + storage: { + Tables: { + buckets: { + Row: { + allowed_mime_types: string[] | null; + avif_autodetection: boolean | null; + created_at: string | null; + file_size_limit: number | null; + id: string; + name: string; + owner: string | null; + public: boolean | null; + updated_at: string | null; + }; + Insert: { + allowed_mime_types?: string[] | null; + avif_autodetection?: boolean | null; + created_at?: string | null; + file_size_limit?: number | null; + id: string; + name: string; + owner?: string | null; + public?: boolean | null; + updated_at?: string | null; + }; + Update: { + allowed_mime_types?: string[] | null; + avif_autodetection?: boolean | null; + created_at?: string | null; + file_size_limit?: number | null; + id?: string; + name?: string; + owner?: string | null; + public?: boolean | null; + updated_at?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'buckets_owner_fkey'; + columns: ['owner']; + referencedRelation: 'users'; + referencedColumns: ['id']; + }, + ]; + }; + migrations: { + Row: { + executed_at: string | null; + hash: string; + id: number; + name: string; + }; + Insert: { + executed_at?: string | null; + hash: string; + id: number; + name: string; + }; + Update: { + executed_at?: string | null; + hash?: string; + id?: number; + name?: string; + }; + Relationships: []; + }; + objects: { + Row: { + bucket_id: string | null; + created_at: string | null; + id: string; + last_accessed_at: string | null; + metadata: Json | null; + name: string | null; + owner: string | null; + path_tokens: string[] | null; + updated_at: string | null; + version: string | null; + }; + Insert: { + bucket_id?: string | null; + created_at?: string | null; + id?: string; + last_accessed_at?: string | null; + metadata?: Json | null; + name?: string | null; + owner?: string | null; + path_tokens?: string[] | null; + updated_at?: string | null; + version?: string | null; + }; + Update: { + bucket_id?: string | null; + created_at?: string | null; + id?: string; + last_accessed_at?: string | null; + metadata?: Json | null; + name?: string | null; + owner?: string | null; + path_tokens?: string[] | null; + updated_at?: string | null; + version?: string | null; + }; + Relationships: [ + { + foreignKeyName: 'objects_bucketId_fkey'; + columns: ['bucket_id']; + referencedRelation: 'buckets'; + referencedColumns: ['id']; + }, + ]; + }; + }; + Views: { + [_ in never]: never; + }; + Functions: { + can_insert_object: { + Args: { + bucketid: string; + name: string; + owner: string; + metadata: Json; + }; + Returns: undefined; + }; + extension: { + Args: { + name: string; + }; + Returns: string; + }; + filename: { + Args: { + name: string; + }; + Returns: string; + }; + foldername: { + Args: { + name: string; + }; + Returns: unknown; + }; + get_size_by_bucket: { + Args: Record; + Returns: { + size: number; + bucket_id: string; + }[]; + }; + search: { + Args: { + prefix: string; + bucketname: string; + limits?: number; + levels?: number; + offsets?: number; + search?: string; + sortcolumn?: string; + sortorder?: string; + }; + Returns: { + name: string; + id: string; + updated_at: string; + created_at: string; + last_accessed_at: string; + metadata: Json; + }[]; + }; + }; + Enums: { + [_ in never]: never; + }; + CompositeTypes: { + [_ in never]: never; + }; + }; +} diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.tsx new file mode 100644 index 00000000..e6f5d851 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.tsx @@ -0,0 +1,133 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; +import { fireEvent, screen } from '@testing-library/react'; +import React, { useState } from 'react'; + +import { useDeleteManyMutation, useQuery } from '../../src'; +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-delmany'; + +describe('useDeleteManyMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + let contacts: Database['public']['Tables']['contact']['Row'][]; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + }); + + beforeEach(async () => { + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + + const { data } = await client + .from('contact') + .insert( + new Array(3) + .fill(0) + .map((idx) => ({ username: `${testRunPrefix}-${idx}` })), + ) + .select('*'); + contacts = data as Database['public']['Tables']['contact']['Row'][]; + }); + + it('should delete existing cache item and reduce count', async () => { + const queryClient = new QueryClient(); + function Page() { + const [success, setSuccess] = useState(false); + const { data, count } = useQuery( + client + .from('contact') + .select('id,username', { count: 'exact' }) + .eq('username', contacts[0].username ?? ''), + ); + const { mutateAsync: deleteContact } = useDeleteManyMutation( + client.from('contact'), + ['id'], + null, + { onSuccess: () => setSuccess(true) }, + ); + const { mutateAsync: deleteWithEmptyOptions } = useDeleteManyMutation( + client.from('contact'), + ['id'], + null, + {}, + ); + const { mutateAsync: deleteWithoutOptions } = useDeleteManyMutation( + client.from('contact'), + ['id'], + ); + return ( +
+
+ await deleteContact([ + { + id: (data ?? []).find((c) => c)?.id, + }, + ]) + } + /> +
+ await deleteWithEmptyOptions([ + { + id: (data ?? []).find((c) => c)?.id, + }, + ]) + } + /> +
+ await deleteWithoutOptions([ + { + id: (data ?? []).find((c) => c)?.id, + }, + ]) + } + /> + {(data ?? []).map((d) => ( + {d.username} + ))} + {`count: ${count}`} + {`success: ${success}`} +
+ ); + } + + renderWithConfig(, queryClient); + await screen.findByText( + `count: ${contacts.length}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('deleteWithEmptyOptions')); + await screen.findByText( + `count: ${contacts.length - 1}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('deleteWithoutOptions')); + await screen.findByText( + `count: ${contacts.length - 2}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('delete')); + await screen.findByText('success: true', {}, { timeout: 10000 }); + await screen.findByText( + `count: ${contacts.length - 3}`, + {}, + { timeout: 10000 }, + ); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.tsx new file mode 100644 index 00000000..43a02694 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.tsx @@ -0,0 +1,217 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; +import { fireEvent, screen } from '@testing-library/react'; +import React, { useState } from 'react'; + +import { useDeleteMutation, useQuery } from '../../src'; +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-delete'; + +describe('useDeleteMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + let contacts: Database['public']['Tables']['contact']['Row'][]; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + }); + + beforeEach(async () => { + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + + const { data } = await client + .from('contact') + .insert( + new Array(3) + .fill(0) + .map((idx) => ({ username: `${testRunPrefix}-${idx}` })), + ) + .select('*'); + contacts = data as Database['public']['Tables']['contact']['Row'][]; + }); + + it('should invalidate address_book cache after delete', async () => { + const { data: addressBooks } = await client + .from('address_book') + .insert([ + { + name: 'hello', + }, + ]) + .select('id'); + + const addressBookId = addressBooks ? addressBooks[0].id : ''; + + await client.from('address_book_contact').insert([ + { + address_book: addressBookId, + contact: contacts[0].id, + }, + { + address_book: addressBookId, + contact: contacts[1].id, + }, + ]); + + const queryClient = new QueryClient(); + function Page() { + const { data: addressBookAndContact } = useQuery( + client + .from('address_book') + .select('id, name, contacts:contact (id, username)') + .eq('id', addressBookId) + .single(), + ); + + const { mutateAsync: deleteContactFromAddressBook } = useDeleteMutation( + client.from('address_book_contact'), + ['contact', 'address_book'], + 'contact, address_book', + { + revalidateRelations: [ + { + relation: 'address_book', + relationIdColumn: 'id', + fKeyColumn: 'address_book', + }, + ], + }, + ); + + return ( +
+ {addressBookAndContact?.name} + + count: {addressBookAndContact?.contacts.length} + + {addressBookAndContact?.contacts.map((contact) => { + return ( +
+ {contact.username} + +
+ ); + })} +
+ ); + } + + renderWithConfig(, queryClient); + + await screen.findByText(`hello`, {}, { timeout: 10000 }); + + await screen.findByText(`count: 2`, {}, { timeout: 10000 }); + + const deleteButtons = screen.getAllByRole(`button`, { + name: /Delete Contact/i, + }); + + fireEvent.click(deleteButtons[0]); + + await screen.findByText(`count: 1`, {}, { timeout: 10000 }); + }); + + it('should delete existing cache item and reduce count', async () => { + const queryClient = new QueryClient(); + function Page() { + const [success, setSuccess] = useState(false); + const { data, count } = useQuery( + client + .from('contact') + .select('id,username', { count: 'exact' }) + .eq('username', contacts[0].username ?? ''), + ); + const { mutateAsync: deleteContact } = useDeleteMutation( + client.from('contact'), + ['id'], + null, + { onSuccess: () => setSuccess(true) }, + ); + const { mutateAsync: deleteWithEmptyOptions } = useDeleteMutation( + client.from('contact'), + ['id'], + null, + {}, + ); + const { mutateAsync: deleteWithoutOptions } = useDeleteMutation( + client.from('contact'), + ['id'], + ); + return ( +
+
+ await deleteContact({ + id: (data ?? []).find((c) => c)?.id, + }) + } + /> +
+ await deleteWithEmptyOptions({ + id: (data ?? []).find((c) => c)?.id, + }) + } + /> +
+ await deleteWithoutOptions({ + id: (data ?? []).find((c) => c)?.id, + }) + } + /> + {(data ?? []).map((d) => ( + {d.username} + ))} + {`count: ${count}`} + {`success: ${success}`} +
+ ); + } + + renderWithConfig(, queryClient); + await screen.findByText( + `count: ${contacts.length}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('deleteWithEmptyOptions')); + await screen.findByText( + `count: ${contacts.length - 1}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('deleteWithoutOptions')); + await screen.findByText( + `count: ${contacts.length - 2}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('delete')); + await screen.findByText('success: true', {}, { timeout: 10000 }); + await screen.findByText( + `count: ${contacts.length - 3}`, + {}, + { timeout: 10000 }, + ); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.tsx new file mode 100644 index 00000000..e13f47c5 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.tsx @@ -0,0 +1,79 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; +import { fireEvent, screen } from '@testing-library/react'; +import React, { useState } from 'react'; + +import { useInsertMutation, useQuery } from '../../src'; +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-insert'; + +describe('useInsertMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + it('should insert into existing cache item with alias', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-1`; + const USERNAME_2 = `${testRunPrefix}-2`; + const USERNAME_3 = `${testRunPrefix}-3`; + function Page() { + const [success, setSuccess] = useState(false); + const { data, count } = useQuery( + client + .from('contact') + .select('id,alias:username', { count: 'exact' }) + .in('username', [USERNAME_1, USERNAME_2, USERNAME_3]), + ); + const { mutateAsync: insert } = useInsertMutation( + client.from('contact'), + ['id'], + null, + { + onSuccess: () => setSuccess(true), + }, + ); + + return ( +
+
+ await insert([ + { + username: USERNAME_2, + }, + { + username: USERNAME_3, + }, + ]) + } + /> + {(data ?? []).map((d) => ( + {d.alias} + ))} + {`count: ${count}`} + {`success: ${success}`} +
+ ); + } + + renderWithConfig(, queryClient); + await screen.findByText('count: 0', {}, { timeout: 10000 }); + fireEvent.click(screen.getByTestId('insertMany')); + await screen.findByText(USERNAME_2, {}, { timeout: 10000 }); + await screen.findByText(USERNAME_3, {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 2'); + await screen.findByText('success: true', {}, { timeout: 10000 }); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.tsx new file mode 100644 index 00000000..da01a5f9 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.tsx @@ -0,0 +1,87 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; +import { fireEvent, screen } from '@testing-library/react'; +import React, { useState } from 'react'; + +import { useInsertMutation, useQuery, useUpdateMutation } from '../../src'; +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-update'; + +describe('useUpdateMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + it('should update existing cache item', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-2`; + const USERNAME_2 = `${testRunPrefix}-3`; + function Page() { + const [success, setSuccess] = useState(false); + const { data, count } = useQuery( + client + .from('contact') + .select('id,username', { count: 'exact' }) + .in('username', [USERNAME_1, USERNAME_2]), + ); + const { mutateAsync: insert } = useInsertMutation( + client.from('contact'), + ['id'], + ); + const { mutateAsync: update } = useUpdateMutation( + client.from('contact'), + ['id'], + null, + { + onSuccess: () => setSuccess(true), + }, + ); + return ( +
+
await insert([{ username: USERNAME_1 }])} + /> +
+ await update({ + id: (data ?? []).find((d) => d.username === USERNAME_1)?.id, + username: USERNAME_2, + }) + } + /> + + { + data?.find((d) => + [USERNAME_1, USERNAME_2].includes(d.username ?? ''), + )?.username + } + + {`count: ${count}`} + {`success: ${success}`} +
+ ); + } + + renderWithConfig(, queryClient); + await screen.findByText('count: 0', {}, { timeout: 10000 }); + fireEvent.click(screen.getByTestId('insert')); + await screen.findByText(USERNAME_1, {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + fireEvent.click(screen.getByTestId('update')); + await screen.findByText(USERNAME_2, {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + await screen.findByText('success: true', {}, { timeout: 10000 }); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.tsx new file mode 100644 index 00000000..1c4ef019 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.tsx @@ -0,0 +1,92 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; +import { fireEvent, screen } from '@testing-library/react'; +import React, { useState } from 'react'; + +import { useQuery, useUpsertMutation } from '../../src'; +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-upsert'; + +describe('useUpsertMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + it('should upsert into existing cache item', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-2`; + const USERNAME_2 = `${testRunPrefix}-3`; + function Page() { + const [success, setSuccess] = useState(false); + const { data, count } = useQuery( + client + .from('contact') + .select('id,username,golden_ticket', { count: 'exact' }) + .in('username', [USERNAME_1, USERNAME_2]), + ); + + const { mutateAsync: upsert } = useUpsertMutation( + client.from('contact'), + ['id'], + null, + { + onSuccess: () => setSuccess(true), + }, + ); + + return ( +
+
+ await upsert([ + { + id: data?.find((d) => d.username === USERNAME_1)?.id, + username: USERNAME_1, + golden_ticket: true, + }, + { + id: 'cae53d23-51a8-4408-9f40-05c83a4b0bbd', + username: USERNAME_2, + golden_ticket: null, + }, + ]) + } + /> + {(data ?? []).map((d) => ( + + {`${d.username} - ${d.golden_ticket ?? 'null'}`} + + ))} + {`count: ${count}`} + {`success: ${success}`} +
+ ); + } + + await client + .from('contact') + .insert({ + username: USERNAME_1, + golden_ticket: true, + }) + .throwOnError(); + renderWithConfig(, queryClient); + await screen.findByText('count: 1', {}, { timeout: 10000 }); + fireEvent.click(screen.getByTestId('upsertMany')); + await screen.findByText(`${USERNAME_1} - true`, {}, { timeout: 10000 }); + await screen.findByText(`${USERNAME_2} - null`, {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 2'); + await screen.findByText('success: true', {}, { timeout: 10000 }); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/query/fetch.spec.ts b/packages/postgrest-vue-query/__tests__/query/fetch.spec.ts new file mode 100644 index 00000000..0e46238e --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/query/fetch.spec.ts @@ -0,0 +1,45 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; + +import { fetchQuery } from '../../src'; +import type { Database } from '../database.types'; +import '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-fetch'; + +describe('fetchQuery', () => { + let client: SupabaseClient; + let testRunPrefix: string; + let contacts: Database['public']['Tables']['contact']['Row'][]; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + + const { data } = await client + .from('contact') + .insert([ + { username: `${testRunPrefix}-username-1` }, + { username: `${testRunPrefix}-username-2` }, + { username: `${testRunPrefix}-username-3` }, + { username: `${testRunPrefix}-username-4` }, + ]) + .select('*') + .throwOnError(); + contacts = data ?? []; + expect(contacts).toHaveLength(4); + }); + + it('fetchQuery should work', async () => { + const queryClient = new QueryClient(); + const { data } = await fetchQuery( + queryClient, + client.from('contact').select('*').ilike('username', `${testRunPrefix}%`), + ); + expect(data).toEqual(contacts); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/query/prefetch.integration.spec.ts b/packages/postgrest-vue-query/__tests__/query/prefetch.integration.spec.ts new file mode 100644 index 00000000..feaef48b --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/query/prefetch.integration.spec.ts @@ -0,0 +1,53 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; + +import { fetchQueryInitialData, prefetchQuery } from '../../src'; +import type { Database } from '../database.types'; +import '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-prefetch'; + +describe('prefetch', () => { + let client: SupabaseClient; + let testRunPrefix: string; + let contacts: Database['public']['Tables']['contact']['Row'][]; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + + const { data } = await client + .from('contact') + .insert([ + { username: `${testRunPrefix}-username-1` }, + { username: `${testRunPrefix}-username-2` }, + { username: `${testRunPrefix}-username-3` }, + { username: `${testRunPrefix}-username-4` }, + ]) + .select('*') + .throwOnError(); + contacts = data ?? []; + expect(contacts).toHaveLength(4); + }); + + it('prefetchQuery should throw if query is not a PostgrestBuilder', async () => { + const queryClient = new QueryClient(); + try { + await prefetchQuery(queryClient, Promise.resolve({} as any)); + } catch (error) { + expect(error).toEqual(new Error('Key is not a PostgrestBuilder')); + } + }); + + it('fetchQueryInitialData should throw if query is not a PostgrestBuilder', async () => { + try { + await fetchQueryInitialData(Promise.resolve({} as any)); + } catch (error) { + expect(error).toEqual(new Error('Query is not a PostgrestBuilder')); + } + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.tsx new file mode 100644 index 00000000..1b38cbe9 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.tsx @@ -0,0 +1,226 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; +import { fireEvent, screen } from '@testing-library/react'; +import React, { useState } from 'react'; + +import { fetchQueryInitialData, prefetchQuery, useQuery } from '../../src'; +import { encode } from '../../src/lib/key'; +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-query'; + +describe('useQuery', () => { + let client: SupabaseClient; + let testRunPrefix: string; + let contacts: Database['public']['Tables']['contact']['Row'][]; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + + const { data } = await client + .from('contact') + .insert([ + { username: `${testRunPrefix}-username-1` }, + { username: `${testRunPrefix}-username-2` }, + { username: `${testRunPrefix}-username-3` }, + { username: `${testRunPrefix}-username-4` }, + ]) + .select('*') + .throwOnError(); + contacts = data ?? []; + expect(contacts).toHaveLength(4); + }); + + it('should work for single', async () => { + const queryClient = new QueryClient(); + const query = client + .from('contact') + .select('id,username') + .eq('username', contacts[0].username ?? '') + .single(); + function Page() { + const { data } = useQuery(query); + + return
{data?.username}
; + } + + renderWithConfig(, queryClient); + await screen.findByText( + contacts[0].username as string, + {}, + { timeout: 10000 }, + ); + expect(queryClient.getQueryData(encode(query, false))).toBeDefined(); + }); + + it('should work for maybeSingle', async () => { + const queryClient = new QueryClient(); + const query = client + .from('contact') + .select('id,username') + .eq('username', 'unknown') + .maybeSingle(); + function Page() { + const { data, isLoading } = useQuery(query); + return ( +
{isLoading ? 'validating' : `username: ${data?.username}`}
+ ); + } + + renderWithConfig(, queryClient); + await screen.findByText('username: undefined', {}, { timeout: 10000 }); + expect(queryClient.getQueryData(encode(query, false))).toBeDefined(); + }); + + it('should work with multiple', async () => { + const queryClient = new QueryClient(); + const query = client + .from('contact') + .select('id,username', { count: 'exact' }) + .ilike('username', `${testRunPrefix}%`); + function Page() { + const { data, count } = useQuery(query); + return ( +
+
+ { + (data ?? []).find((d) => d.username === contacts[0].username) + ?.username + } +
+
{count}
+
+ ); + } + + renderWithConfig(, queryClient); + await screen.findByText( + contacts[0].username as string, + {}, + { timeout: 10000 }, + ); + expect(screen.getByTestId('count').textContent).toEqual('4'); + expect(queryClient.getQueryData(encode(query, false))).toBeDefined(); + }); + + it('should work for with conditional query', async () => { + const queryClient = new QueryClient(); + function Page() { + const [condition, setCondition] = useState(false); + const { data, isLoading } = useQuery( + client + .from('contact') + .select('id,username') + .eq('username', contacts[0].username ?? '') + .maybeSingle(), + { enabled: condition }, + ); + + return ( +
+
setCondition(true)} /> +
{data?.username ?? 'undefined'}
+
{`isLoading: ${isLoading}`}
+
+ ); + } + + renderWithConfig(, queryClient); + await screen.findByText('undefined', {}, { timeout: 10000 }); + fireEvent.click(screen.getByTestId('setCondition')); + await screen.findByText( + contacts[0].username as string, + {}, + { timeout: 10000 }, + ); + }); + + it('refetch should work', async () => { + const queryClient = new QueryClient(); + function Page() { + const { data, refetch, isLoading } = useQuery( + client + .from('contact') + .select('id,username') + .eq('username', contacts[0].username ?? '') + .single(), + ); + const [refetched, setRefetched] = useState(null); + + return ( +
+
{ + setRefetched((await refetch())?.data?.data); + }} + /> +
{data?.username ?? 'undefined'}
+
{`refetched: ${!!refetched}`}
+
{`isLoading: ${isLoading}`}
+
+ ); + } + + renderWithConfig(, queryClient); + await screen.findByText('isLoading: false', {}, { timeout: 10000 }); + fireEvent.click(screen.getByTestId('mutate')); + await screen.findByText('refetched: true', {}, { timeout: 10000 }); + }); + + it('prefetch should work', async () => { + const queryClient = new QueryClient(); + const query = client + .from('contact') + .select('id,username') + .eq('username', contacts[0].username ?? '') + .single(); + await prefetchQuery(queryClient, query); + let hasBeenFalse = false; + function Page() { + const { data } = useQuery(query); + if (!data) hasBeenFalse = true; + + return ( +
+
{data?.username ?? 'undefined'}
+
+ ); + } + + renderWithConfig(, queryClient); + expect(hasBeenFalse).toBe(false); + await screen.findByText(contacts[0].username!, {}, { timeout: 10000 }); + }); + + it('initalData should work', async () => { + const queryClient = new QueryClient(); + const query = client + .from('contact') + .select('id,username') + .eq('username', contacts[0].username ?? '') + .single(); + const [key, initial] = await fetchQueryInitialData(query); + let hasBeenFalse = false; + function Page() { + const { data } = useQuery(query, { initialData: initial }); + if (!data) hasBeenFalse = true; + + return ( +
+
{data?.username ?? 'undefined'}
+
+ ); + } + + renderWithConfig(, queryClient); + expect(hasBeenFalse).toBe(false); + await screen.findByText(contacts[0].username!, {}, { timeout: 10000 }); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query-integration.spec.tsx b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query-integration.spec.tsx new file mode 100644 index 00000000..a5fde1c1 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query-integration.spec.tsx @@ -0,0 +1,112 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; +import { act, screen } from '@testing-library/react'; +import React, { useState } from 'react'; + +import { useSubscriptionQuery, useQuery } from '../../src'; +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-subscription-query'; + +describe('useSubscriptionQuery', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + afterEach(async () => { + if (client) await client.removeAllChannels(); + }); + + it('should properly update cache', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-1`; + function Page() { + const { data, count } = useQuery( + client + .from('contact') + .select('id,username,has_low_ticket_number,ticket_number', { + count: 'exact', + }) + .eq('username', USERNAME_1), + ); + + const [cbCalled, setCbCalled] = useState(false); + + const { status } = useSubscriptionQuery( + client, + `public:contact:username=eq.${USERNAME_1}`, + { + event: '*', + table: 'contact', + schema: 'public', + filter: `username=eq.${USERNAME_1}`, + }, + ['id'], + 'id,username,has_low_ticket_number,ticket_number', + { + callback: (evt) => { + if (evt.data.ticket_number === 1000) { + setCbCalled(true); + } + }, + }, + ); + + return ( +
+ {(data ?? []).map((d) => ( + {`ticket_number: ${d.ticket_number} | has_low_ticket_number: ${d.has_low_ticket_number}`} + ))} + {`count: ${count}`} + {status} + {`cbCalled: ${cbCalled}`} +
+ ); + } + + const { unmount } = renderWithConfig(, queryClient); + await screen.findByText('count: 0', {}, { timeout: 10000 }); + await screen.findByText('SUBSCRIBED', {}, { timeout: 10000 }); + await new Promise((resolve) => setTimeout(resolve, 2000)); + await act(async () => { + await client + .from('contact') + .insert({ username: USERNAME_1, ticket_number: 1 }) + .select('*') + .throwOnError() + .single(); + }); + await screen.findByText( + 'ticket_number: 1 | has_low_ticket_number: true', + {}, + { timeout: 10000 }, + ); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + await act(async () => { + await client + .from('contact') + .update({ ticket_number: 1000 }) + .eq('username', USERNAME_1) + .throwOnError(); + }); + await screen.findByText( + 'ticket_number: 1000 | has_low_ticket_number: false', + {}, + { timeout: 10000 }, + ); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + await screen.findByText('cbCalled: true', {}, { timeout: 10000 }); + unmount(); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.tsx new file mode 100644 index 00000000..5e1b5709 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.tsx @@ -0,0 +1,92 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/react-query'; +import { act, screen } from '@testing-library/react'; +import React, { useState } from 'react'; + +import { useSubscription, useQuery } from '../../src'; +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-react-query-subscription-plain'; + +describe('useSubscription', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + afterEach(async () => { + if (client) await client.removeAllChannels(); + }); + + it('should properly update cache', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-1`; + function Page() { + const { data, count } = useQuery( + client + .from('contact') + .select('id,username,ticket_number', { count: 'exact' }) + .eq('username', USERNAME_1), + ); + + const [cbCalled, setCbCalled] = useState(false); + + const { status } = useSubscription( + client, + `public:contact:username=eq.${USERNAME_1}`, + { + event: '*', + table: 'contact', + schema: 'public', + filter: `username=eq.${USERNAME_1}`, + }, + ['id'], + { callback: () => setCbCalled(true) }, + ); + + return ( +
+ {(data ?? []).map((d) => ( + {`ticket_number: ${d.ticket_number}`} + ))} + {`count: ${count}`} + {status} + {`cbCalled: ${cbCalled}`} +
+ ); + } + + const { unmount } = renderWithConfig(, queryClient); + await screen.findByText('count: 0', {}, { timeout: 10000 }); + await screen.findByText('SUBSCRIBED', {}, { timeout: 10000 }); + await act(async () => { + await client + .from('contact') + .insert({ username: USERNAME_1, ticket_number: 1 }) + .select('id') + .throwOnError() + .single(); + }); + await screen.findByText('ticket_number: 1', {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + await act(async () => { + await client + .from('contact') + .update({ ticket_number: 5 }) + .eq('username', USERNAME_1) + .throwOnError(); + }); + await screen.findByText('ticket_number: 5', {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + await screen.findByText('cbCalled: true', {}, { timeout: 10000 }); + unmount(); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/utils.tsx b/packages/postgrest-vue-query/__tests__/utils.tsx new file mode 100644 index 00000000..8f0cadb4 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/utils.tsx @@ -0,0 +1,21 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render } from '@testing-library/react'; +import * as dotenv from 'dotenv'; +import { resolve } from 'node:path'; +import React from 'react'; + +dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); + +export const renderWithConfig = ( + element: React.ReactElement, + queryClient: QueryClient, +): ReturnType => { + const TestQueryClientProvider = ({ + children, + }: { + children: React.ReactNode; + }) => ( + {children} + ); + return render(element, { wrapper: TestQueryClientProvider }); +}; diff --git a/packages/postgrest-vue-query/package.json b/packages/postgrest-vue-query/package.json new file mode 100644 index 00000000..8f37a493 --- /dev/null +++ b/packages/postgrest-vue-query/package.json @@ -0,0 +1,82 @@ +{ + "name": "@supabase-cache-helpers/postgrest-vue-query", + "version": "0.0.1", + "author": "Christian Pannwitz ", + "homepage": "https://supabase-cache-helpers.vercel.app", + "bugs": { + "url": "https://github.com/psteinroe/supabase-cache-helpers/issues" + }, + "main": "./dist/index.js", + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "license": "MIT", + "scripts": { + "build": "tsup", + "test": "jest --coverage --runInBand", + "clean": "rm -rf .turbo && rm -rf lint-results && rm -rf .nyc_output && rm -rf node_modules && rm -rf dist", + "lint": "eslint src/**", + "lint:report": "eslint {src/**,__tests__/**} --format json --output-file ./lint-results/postgrest-vue-query.json", + "lint:fix": "eslint {src/**,__tests__/**} --fix", + "typecheck": "tsc --pretty --noEmit", + "format:write": "prettier --write \"{src/**/*.{ts,tsx,md},__tests__/**/*.{ts,tsx,md}}\"", + "format:check": "prettier --check \"{src/**/*.{ts,tsx,md},__tests__/**/*.{ts,tsx,md}}\"" + }, + "keywords": [ + "Supabase", + "PostgREST", + "Cache", + "Tanstack Query", + "Vue Query" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/psteinroe/supabase-cache-helpers.git", + "directory": "packages/postgrest-vue-query" + }, + "peerDependencies": { + "@tanstack/vue-query": "^5.28.13", + "vue": "^3.4.21", + "@supabase/postgrest-js": "^1.9.0" + }, + "jest": { + "preset": "@supabase-cache-helpers/jest-presets/jest/node" + }, + "devDependencies": { + "@supabase/supabase-js": "2.38.5", + "@supabase/postgrest-js": "1.9.0", + "@testing-library/vue": "8.0.3", + "@testing-library/jest-dom": "6.4.0", + "jest-environment-jsdom": "29.7.0", + "@types/jest": "29.5.0", + "dotenv": "16.4.0", + "eslint": "8.54.0", + "@supabase-cache-helpers/eslint-config-custom": "workspace:*", + "@supabase-cache-helpers/prettier-config": "workspace:*", + "jest": "29.7.0", + "@supabase-cache-helpers/jest-presets": "workspace:*", + "ts-jest": "29.1.0", + "@supabase-cache-helpers/tsconfig": "workspace:*", + "@types/flat": "5.0.2", + "tsup": "8.0.0", + "vue": "3.4.21", + "typescript": "5.4.2" + }, + "dependencies": { + "@supabase-cache-helpers/postgrest-core": "workspace:*", + "flat": "5.0.2" + } +} diff --git a/packages/postgrest-vue-query/prettier.config.cjs b/packages/postgrest-vue-query/prettier.config.cjs new file mode 100644 index 00000000..3fb75475 --- /dev/null +++ b/packages/postgrest-vue-query/prettier.config.cjs @@ -0,0 +1 @@ +module.exports = require("@supabase-cache-helpers/prettier-config/prettier.config.js"); diff --git a/packages/postgrest-vue-query/src/cache/index.ts b/packages/postgrest-vue-query/src/cache/index.ts new file mode 100644 index 00000000..c2ef6a83 --- /dev/null +++ b/packages/postgrest-vue-query/src/cache/index.ts @@ -0,0 +1,3 @@ +export * from './use-delete-item'; +export * from './use-mutate-item'; +export * from './use-upsert-item'; diff --git a/packages/postgrest-vue-query/src/cache/use-delete-item.ts b/packages/postgrest-vue-query/src/cache/use-delete-item.ts new file mode 100644 index 00000000..e41bf0c5 --- /dev/null +++ b/packages/postgrest-vue-query/src/cache/use-delete-item.ts @@ -0,0 +1,40 @@ +import { + deleteItem, + DeleteItemOperation, +} from '@supabase-cache-helpers/postgrest-core'; +import { useQueryClient } from '@tanstack/vue-query'; +import flatten from 'flat'; + +import { decode, usePostgrestFilterCache } from '../lib'; + +/** + * Convenience hook to delete an item from the vue query cache. Does not make any http requests, and is supposed to be used for custom cache updates. + * @param opts The mutation options + * @returns void + */ +export function useDeleteItem>( + opts: Omit, 'input'>, +) { + const queryClient = useQueryClient(); + const getPostgrestFilter = usePostgrestFilterCache(); + + return async (input: Type) => + await deleteItem( + { + input: flatten(input) as Type, + ...opts, + }, + { + cacheKeys: queryClient + .getQueryCache() + .getAll() + .map((c) => c.queryKey), + getPostgrestFilter, + revalidate: (key) => queryClient.invalidateQueries({ queryKey: key }), + mutate: (key, fn) => { + queryClient.setQueriesData({ queryKey: key }, fn); + }, + decode, + }, + ); +} diff --git a/packages/postgrest-vue-query/src/cache/use-mutate-item.ts b/packages/postgrest-vue-query/src/cache/use-mutate-item.ts new file mode 100644 index 00000000..619405e5 --- /dev/null +++ b/packages/postgrest-vue-query/src/cache/use-mutate-item.ts @@ -0,0 +1,41 @@ +import { + mutateItem, + MutateItemOperation, +} from '@supabase-cache-helpers/postgrest-core'; +import { useQueryClient } from '@tanstack/vue-query'; +import flatten from 'flat'; + +import { decode, usePostgrestFilterCache } from '../lib'; + +/** + * Convenience hook to mutate an item within the vue query cache. Does not make any http requests, and is supposed to be used for custom cache updates. + * @param opts The mutation options + * @returns void + */ +export function useMutateItem>( + opts: Omit, 'input' | 'mutate'>, +): (input: Partial, mutateFn: (current: Type) => Type) => Promise { + const queryClient = useQueryClient(); + const getPostgrestFilter = usePostgrestFilterCache(); + + return async (input: Partial, mutateFn: (current: Type) => Type) => + await mutateItem( + { + input: flatten(input) as Partial, + mutate: mutateFn, + ...opts, + }, + { + cacheKeys: queryClient + .getQueryCache() + .getAll() + .map((c) => c.queryKey), + getPostgrestFilter, + revalidate: (key) => queryClient.invalidateQueries({ queryKey: key }), + mutate: (key, fn) => { + queryClient.setQueriesData({ queryKey: key }, fn); + }, + decode, + }, + ); +} diff --git a/packages/postgrest-vue-query/src/cache/use-upsert-item.ts b/packages/postgrest-vue-query/src/cache/use-upsert-item.ts new file mode 100644 index 00000000..9027e88f --- /dev/null +++ b/packages/postgrest-vue-query/src/cache/use-upsert-item.ts @@ -0,0 +1,40 @@ +import { + upsertItem, + UpsertItemOperation, +} from '@supabase-cache-helpers/postgrest-core'; +import { useQueryClient } from '@tanstack/vue-query'; +import flatten from 'flat'; + +import { decode, usePostgrestFilterCache } from '../lib'; + +/** + * Convenience hook to upsert an item into the vue query cache. Does not make any http requests, and is supposed to be used for custom cache updates. + * @param opts The mutation options + * @returns void + */ +export function useUpsertItem>( + opts: Omit, 'input'>, +) { + const queryClient = useQueryClient(); + const getPostgrestFilter = usePostgrestFilterCache(); + + return async (input: Type) => + await upsertItem( + { + input: flatten(input) as Type, + ...opts, + }, + { + cacheKeys: queryClient + .getQueryCache() + .getAll() + .map((c) => c.queryKey), + getPostgrestFilter, + revalidate: (key) => queryClient.invalidateQueries({ queryKey: key }), + mutate: (key, fn) => { + queryClient.setQueriesData({ queryKey: key }, fn); + }, + decode, + }, + ); +} diff --git a/packages/postgrest-vue-query/src/index.ts b/packages/postgrest-vue-query/src/index.ts new file mode 100644 index 00000000..7937708f --- /dev/null +++ b/packages/postgrest-vue-query/src/index.ts @@ -0,0 +1,10 @@ +export type { + PostgrestHasMorePaginationCacheData, + PostgrestPaginationCacheData, +} from '@supabase-cache-helpers/postgrest-core'; + +export * from './cache'; +export * from './lib'; +export * from './mutate'; +export * from './query'; +export * from './subscribe'; diff --git a/packages/postgrest-vue-query/src/lib/index.ts b/packages/postgrest-vue-query/src/lib/index.ts new file mode 100644 index 00000000..fef0d0c0 --- /dev/null +++ b/packages/postgrest-vue-query/src/lib/index.ts @@ -0,0 +1,3 @@ +export * from './use-postgrest-filter-cache'; +export * from './key'; +export * from './use-queries-for-table-loader'; diff --git a/packages/postgrest-vue-query/src/lib/key.ts b/packages/postgrest-vue-query/src/lib/key.ts new file mode 100644 index 00000000..aa7795bf --- /dev/null +++ b/packages/postgrest-vue-query/src/lib/key.ts @@ -0,0 +1,71 @@ +import { + PostgrestParser, + DecodedKey, + isPostgrestBuilder, +} from '@supabase-cache-helpers/postgrest-core'; + +export const KEY_PREFIX = 'postgrest'; +export const INFINITE_KEY_PREFIX = 'page'; + +export type DecodedVueQueryKey = DecodedKey & { + isInfinite: boolean; + key: string[]; +}; + +export const encode = (key: unknown, isInfinite: boolean): string[] => { + if (!isPostgrestBuilder(key)) { + throw new Error('Key is not a PostgrestBuilder'); + } + + const parser = new PostgrestParser(key); + return [ + KEY_PREFIX, + isInfinite ? INFINITE_KEY_PREFIX : 'null', + parser.schema, + parser.table, + parser.queryKey, + parser.bodyKey ?? 'null', + `count=${parser.count}`, + `head=${parser.isHead}`, + parser.orderByKey, + ]; +}; + +export const decode = (key: unknown): DecodedVueQueryKey | null => { + if (!Array.isArray(key)) return null; + + const [ + prefix, + infinitePrefix, + schema, + table, + queryKey, + bodyKey, + count, + head, + orderByKey, + ] = key; + + // Exit early if not a postgrest key + if (prefix !== KEY_PREFIX) return null; + + const params = new URLSearchParams(queryKey); + const limit = params.get('limit'); + const offset = params.get('offset'); + + const countValue = count.replace('count=', ''); + + return { + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + bodyKey, + count: countValue === 'null' ? null : countValue, + isHead: head === 'head=true', + isInfinite: infinitePrefix === INFINITE_KEY_PREFIX, + key, + queryKey, + schema, + table, + orderByKey, + }; +}; diff --git a/packages/postgrest-vue-query/src/lib/use-postgrest-filter-cache.ts b/packages/postgrest-vue-query/src/lib/use-postgrest-filter-cache.ts new file mode 100644 index 00000000..9551c173 --- /dev/null +++ b/packages/postgrest-vue-query/src/lib/use-postgrest-filter-cache.ts @@ -0,0 +1,29 @@ +import { + encodeObject, + PostgrestFilter, + PostgrestQueryParserOptions, +} from '@supabase-cache-helpers/postgrest-core'; +import { useQueryClient } from '@tanstack/vue-query'; + +export const POSTGREST_FILTER_KEY_PREFIX = 'postgrest-filter'; + +export const usePostgrestFilterCache = < + R extends Record, +>() => { + const queryClient = useQueryClient(); + + return (query: string, opts?: PostgrestQueryParserOptions) => { + const key = [ + POSTGREST_FILTER_KEY_PREFIX, + query, + opts ? encodeObject(opts) : null, + ]; + const cacheData = queryClient.getQueryData(key); + if (cacheData instanceof PostgrestFilter) { + return cacheData; + } + const filter = PostgrestFilter.fromQuery(query, opts); + queryClient.setQueryData(key, filter); + return filter as PostgrestFilter; + }; +}; diff --git a/packages/postgrest-vue-query/src/lib/use-queries-for-table-loader.ts b/packages/postgrest-vue-query/src/lib/use-queries-for-table-loader.ts new file mode 100644 index 00000000..aa4792ca --- /dev/null +++ b/packages/postgrest-vue-query/src/lib/use-queries-for-table-loader.ts @@ -0,0 +1,26 @@ +import { BuildNormalizedQueryOps } from '@supabase-cache-helpers/postgrest-core'; +import { useQueryClient } from '@tanstack/vue-query'; + +import { decode } from './key'; +import { usePostgrestFilterCache } from './use-postgrest-filter-cache'; + +export const useQueriesForTableLoader = (table: string) => { + const queryClient = useQueryClient(); + const getPostgrestFilter = usePostgrestFilterCache(); + + return () => + queryClient + .getQueryCache() + .getAll() + .map((c) => c.queryKey) + .reduce>( + (prev, curr) => { + const decodedKey = decode(curr); + if (decodedKey?.table === table) { + prev.push(getPostgrestFilter(decodedKey.queryKey).params); + } + return prev; + }, + [], + ); +}; diff --git a/packages/postgrest-vue-query/src/mutate/get-user-response.ts b/packages/postgrest-vue-query/src/mutate/get-user-response.ts new file mode 100644 index 00000000..509d909d --- /dev/null +++ b/packages/postgrest-vue-query/src/mutate/get-user-response.ts @@ -0,0 +1,14 @@ +import { MutationFetcherResponse } from '@supabase-cache-helpers/postgrest-core'; + +type Truthy = T extends false | '' | 0 | null | undefined ? never : T; // from lodash + +export function truthy(value: T): value is Truthy { + return !!value; +} + +export const getUserResponse = ( + d: MutationFetcherResponse[] | null | undefined, +) => { + if (!d) return d; + return d.map((r) => r.userQueryData).filter(truthy); +}; diff --git a/packages/postgrest-vue-query/src/mutate/index.ts b/packages/postgrest-vue-query/src/mutate/index.ts new file mode 100644 index 00000000..9f7c605c --- /dev/null +++ b/packages/postgrest-vue-query/src/mutate/index.ts @@ -0,0 +1,5 @@ +export * from './use-delete-many-mutation'; +export * from './use-delete-mutation'; +export * from './use-insert-mutation'; +export * from './use-update-mutation'; +export * from './use-upsert-mutation'; diff --git a/packages/postgrest-vue-query/src/mutate/types.ts b/packages/postgrest-vue-query/src/mutate/types.ts new file mode 100644 index 00000000..92e6778c --- /dev/null +++ b/packages/postgrest-vue-query/src/mutate/types.ts @@ -0,0 +1,91 @@ +import { GetResult } from '@supabase/postgrest-js/dist/module/select-query-parser'; +import { + GenericSchema, + GenericTable, +} from '@supabase/postgrest-js/dist/module/types'; +import { PostgrestError } from '@supabase/supabase-js'; +import { + DeleteFetcherOptions, + InsertFetcherOptions, + UpdateFetcherOptions, + UpsertFetcherOptions, + RevalidateOpts, +} from '@supabase-cache-helpers/postgrest-core'; +import { UseMutationOptions } from '@tanstack/vue-query'; + +export type Operation = + | 'Insert' + | 'UpdateOne' + | 'Upsert' + | 'DeleteOne' + | 'DeleteMany'; + +export type GetFetcherOptions< + S extends GenericSchema, + T extends GenericTable, + O extends Operation, +> = O extends 'Insert' + ? InsertFetcherOptions + : O extends 'UpdateOne' + ? UpdateFetcherOptions + : O extends 'Upsert' + ? UpsertFetcherOptions + : O extends 'DeleteOne' | 'DeleteMany' + ? DeleteFetcherOptions + : never; + +export type GetInputType< + T extends GenericTable, + O extends Operation, +> = O extends 'DeleteOne' + ? Partial // TODO: Can we pick the primary keys somehow? + : O extends 'DeleteMany' + ? Partial[] + : O extends 'Insert' | 'Upsert' + ? T['Insert'][] + : O extends 'UpdateOne' + ? T['Update'] + : never; + +export type GetReturnType< + S extends GenericSchema, + T extends GenericTable, + RelationName, + Relationships, + O extends Operation, + Q extends string = '*', + R = GetResult< + S, + T['Row'], + RelationName, + Relationships, + Q extends '*' ? '*' : Q + >, +> = O extends 'UpdateOne' + ? R | null + : O extends 'DeleteOne' + ? R | null + : O extends 'Insert' | 'Upsert' | 'DeleteMany' + ? R[] | null + : never; + +export type UsePostgrestMutationOpts< + S extends GenericSchema, + T extends GenericTable, + RelationName, + Relationships, + O extends Operation, + Q extends string = '*', + R = GetResult< + S, + T['Row'], + RelationName, + Relationships, + Q extends '*' ? '*' : Q + >, +> = RevalidateOpts & + UseMutationOptions< + GetReturnType | null, + PostgrestError, + GetInputType + > & { disableAutoQuery?: boolean } & GetFetcherOptions; diff --git a/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts new file mode 100644 index 00000000..d3f25c94 --- /dev/null +++ b/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts @@ -0,0 +1,76 @@ +import { PostgrestQueryBuilder } from '@supabase/postgrest-js'; +import { GetResult } from '@supabase/postgrest-js/dist/module/select-query-parser'; +import { + GenericSchema, + GenericTable, +} from '@supabase/postgrest-js/dist/module/types'; +import { + buildDeleteFetcher, + getTable, +} from '@supabase-cache-helpers/postgrest-core'; +import { useMutation } from '@tanstack/vue-query'; + +import { UsePostgrestMutationOpts } from './types'; +import { useDeleteItem } from '../cache'; +import { useQueriesForTableLoader } from '../lib'; + +/** + * Hook to execute a DELETE mutation + * + * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table + * @param {Array} primaryKeys Array of primary keys of the table + * @param {string | null} query Optional PostgREST query string for the DELETE mutation + * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + */ +function useDeleteManyMutation< + S extends GenericSchema, + T extends GenericTable, + RelationName, + Re = T extends { Relationships: infer R } ? R : unknown, + Q extends string = '*', + R = GetResult, +>( + qb: PostgrestQueryBuilder, + primaryKeys: (keyof T['Row'])[], + query?: Q | null, + opts?: Omit< + UsePostgrestMutationOpts, + 'mutationFn' + >, +) { + const queriesForTable = useQueriesForTableLoader(getTable(qb)); + const deleteItem = useDeleteItem({ + ...opts, + primaryKeys, + table: getTable(qb), + schema: qb.schema as string, + }); + + return useMutation({ + mutationFn: async (input) => { + const result = await buildDeleteFetcher( + qb, + primaryKeys, + { + query: query ?? undefined, + queriesForTable, + disabled: opts?.disableAutoQuery, + ...opts, + }, + )(input); + + if (result) { + for (const r of result) { + deleteItem(r.normalizedData as T['Row']); + } + } + + if (!result || result.every((r) => !r.userQueryData)) return null; + + return result.map((r) => r.userQueryData as R); + }, + ...opts, + }); +} + +export { useDeleteManyMutation }; diff --git a/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts new file mode 100644 index 00000000..ab86a8b0 --- /dev/null +++ b/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts @@ -0,0 +1,75 @@ +import { PostgrestQueryBuilder } from '@supabase/postgrest-js'; +import { GetResult } from '@supabase/postgrest-js/dist/module/select-query-parser'; +import { + GenericSchema, + GenericTable, +} from '@supabase/postgrest-js/dist/module/types'; +import { + buildDeleteFetcher, + getTable, +} from '@supabase-cache-helpers/postgrest-core'; +import { useMutation } from '@tanstack/vue-query'; + +import { UsePostgrestMutationOpts } from './types'; +import { useDeleteItem } from '../cache'; +import { useQueriesForTableLoader } from '../lib'; + +/** + * Hook to execute a DELETE mutation + * + * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table + * @param {Array} primaryKeys Array of primary keys of the table + * @param {string | null} query Optional PostgREST query string for the DELETE mutation + * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + */ +function useDeleteMutation< + S extends GenericSchema, + T extends GenericTable, + RelationName, + Re = T extends { Relationships: infer R } ? R : unknown, + Q extends string = '*', + R = GetResult, +>( + qb: PostgrestQueryBuilder, + primaryKeys: (keyof T['Row'])[], + query?: Q | null, + opts?: Omit< + UsePostgrestMutationOpts, + 'mutationFn' + >, +) { + const queriesForTable = useQueriesForTableLoader(getTable(qb)); + const deleteItem = useDeleteItem({ + ...opts, + primaryKeys, + table: getTable(qb), + schema: qb.schema as string, + }); + + return useMutation({ + mutationFn: async (input) => { + const r = await buildDeleteFetcher( + qb, + primaryKeys, + { + query: query ?? undefined, + queriesForTable, + disabled: opts?.disableAutoQuery, + ...opts, + }, + )([input]); + + if (!r) return null; + + const result = r[0]; + + if (result) { + await deleteItem(result.normalizedData as T['Row']); + } + return result?.userQueryData ?? null; + }, + ...opts, + }); +} + +export { useDeleteMutation }; diff --git a/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts new file mode 100644 index 00000000..6f3f2013 --- /dev/null +++ b/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts @@ -0,0 +1,75 @@ +import { PostgrestQueryBuilder } from '@supabase/postgrest-js'; +import { GetResult } from '@supabase/postgrest-js/dist/module/select-query-parser'; +import { + GenericSchema, + GenericTable, +} from '@supabase/postgrest-js/dist/module/types'; +import { + buildInsertFetcher, + getTable, +} from '@supabase-cache-helpers/postgrest-core'; +import { useMutation } from '@tanstack/vue-query'; + +import { getUserResponse } from './get-user-response'; +import { UsePostgrestMutationOpts } from './types'; +import { useUpsertItem } from '../cache'; +import { useQueriesForTableLoader } from '../lib'; + +/** + * Hook to execute a INSERT mutation + * + * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table + * @param {Array} primaryKeys Array of primary keys of the table + * @param {string | null} query Optional PostgREST query string for the INSERT mutation + * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + */ +function useInsertMutation< + S extends GenericSchema, + T extends GenericTable, + RelationName, + Re = T extends { Relationships: infer R } ? R : unknown, + Q extends string = '*', + R = GetResult, +>( + qb: PostgrestQueryBuilder, + primaryKeys: (keyof T['Row'])[], + query?: Q | null, + opts?: Omit< + UsePostgrestMutationOpts, + 'mutationFn' + >, +) { + const queriesForTable = useQueriesForTableLoader(getTable(qb)); + const upsertItem = useUpsertItem({ + ...opts, + primaryKeys, + table: getTable(qb), + schema: qb.schema as string, + }); + + return useMutation({ + mutationFn: async (input) => { + const result = await buildInsertFetcher( + qb, + { + query: query ?? undefined, + queriesForTable, + disabled: opts?.disableAutoQuery, + ...opts, + }, + )(input); + + if (result) { + await Promise.all( + result.map( + async (d) => await upsertItem(d.normalizedData as T['Row']), + ), + ); + } + return getUserResponse(result) ?? null; + }, + ...opts, + }); +} + +export { useInsertMutation }; diff --git a/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts new file mode 100644 index 00000000..f6e107c3 --- /dev/null +++ b/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts @@ -0,0 +1,70 @@ +import { PostgrestQueryBuilder } from '@supabase/postgrest-js'; +import { GetResult } from '@supabase/postgrest-js/dist/module/select-query-parser'; +import { + GenericSchema, + GenericTable, +} from '@supabase/postgrest-js/dist/module/types'; +import { + buildUpdateFetcher, + getTable, +} from '@supabase-cache-helpers/postgrest-core'; +import { useMutation } from '@tanstack/vue-query'; + +import { UsePostgrestMutationOpts } from './types'; +import { useUpsertItem } from '../cache'; +import { useQueriesForTableLoader } from '../lib'; + +/** + * Hook to execute a UPDATE mutation + * + * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table + * @param {Array} primaryKeys Array of primary keys of the table + * @param {string | null} query Optional PostgREST query string for the UPDATE mutation + * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + */ +function useUpdateMutation< + S extends GenericSchema, + T extends GenericTable, + RelationName, + Re = T extends { Relationships: infer R } ? R : unknown, + Q extends string = '*', + R = GetResult, +>( + qb: PostgrestQueryBuilder, + primaryKeys: (keyof T['Row'])[], + query?: Q | null, + opts?: Omit< + UsePostgrestMutationOpts, + 'mutationFn' + >, +) { + const queriesForTable = useQueriesForTableLoader(getTable(qb)); + const upsertItem = useUpsertItem({ + ...opts, + primaryKeys, + table: getTable(qb), + schema: qb.schema as string, + }); + + return useMutation({ + mutationFn: async (input) => { + const result = await buildUpdateFetcher( + qb, + primaryKeys, + { + query: query ?? undefined, + queriesForTable, + disabled: opts?.disableAutoQuery, + ...opts, + }, + )(input); + if (result) { + await upsertItem(result.normalizedData as T['Row']); + } + return result?.userQueryData ?? null; + }, + ...opts, + }); +} + +export { useUpdateMutation }; diff --git a/packages/postgrest-vue-query/src/mutate/use-upsert-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-upsert-mutation.ts new file mode 100644 index 00000000..718b87f5 --- /dev/null +++ b/packages/postgrest-vue-query/src/mutate/use-upsert-mutation.ts @@ -0,0 +1,69 @@ +import { PostgrestQueryBuilder } from '@supabase/postgrest-js'; +import { GetResult } from '@supabase/postgrest-js/dist/module/select-query-parser'; +import { + GenericSchema, + GenericTable, +} from '@supabase/postgrest-js/dist/module/types'; +import { + buildUpsertFetcher, + getTable, +} from '@supabase-cache-helpers/postgrest-core'; +import { useMutation } from '@tanstack/vue-query'; + +import { getUserResponse } from './get-user-response'; +import { UsePostgrestMutationOpts } from './types'; +import { useUpsertItem } from '../cache'; +import { useQueriesForTableLoader } from '../lib'; + +/** + * Hook to execute a UPSERT mutation + * + * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table + * @param {Array} primaryKeys Array of primary keys of the table + * @param {string | null} query Optional PostgREST query string for the UPSERT mutation + * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + */ +function useUpsertMutation< + S extends GenericSchema, + T extends GenericTable, + RelationName, + Re = T extends { Relationships: infer R } ? R : unknown, + Q extends string = '*', + R = GetResult, +>( + qb: PostgrestQueryBuilder, + primaryKeys: (keyof T['Row'])[], + query?: Q | null, + opts?: Omit< + UsePostgrestMutationOpts, + 'mutationFn' + >, +) { + const queriesForTable = useQueriesForTableLoader(getTable(qb)); + const upsertItem = useUpsertItem({ + ...opts, + primaryKeys, + table: getTable(qb), + schema: qb.schema as string, + }); + + return useMutation({ + mutationFn: async (input: T['Insert'][]) => { + const data = await buildUpsertFetcher(qb, { + query: query ?? undefined, + queriesForTable, + disabled: opts?.disableAutoQuery, + ...opts, + })(input); + if (data) { + await Promise.all( + data.map(async (d) => await upsertItem(d.normalizedData as T['Row'])), + ); + } + return getUserResponse(data) ?? null; + }, + ...opts, + }); +} + +export { useUpsertMutation }; diff --git a/packages/postgrest-vue-query/src/query/build-query-opts.ts b/packages/postgrest-vue-query/src/query/build-query-opts.ts new file mode 100644 index 00000000..c6a005e9 --- /dev/null +++ b/packages/postgrest-vue-query/src/query/build-query-opts.ts @@ -0,0 +1,27 @@ +import { PostgrestError } from '@supabase/postgrest-js'; +import { + AnyPostgrestResponse, + isPostgrestBuilder, +} from '@supabase-cache-helpers/postgrest-core'; +import { UseQueryOptions as UseVueQueryOptions } from '@tanstack/vue-query'; + +import { encode } from '../lib/key'; + +export function buildQueryOpts( + query: PromiseLike>, + config?: Omit< + UseVueQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): UseVueQueryOptions, PostgrestError> { + return { + queryKey: encode(query, false), + queryFn: async () => { + if (isPostgrestBuilder(query)) { + query = query.throwOnError(); + } + return await query; + }, + ...config, + }; +} diff --git a/packages/postgrest-vue-query/src/query/fetch.ts b/packages/postgrest-vue-query/src/query/fetch.ts new file mode 100644 index 00000000..273c40dd --- /dev/null +++ b/packages/postgrest-vue-query/src/query/fetch.ts @@ -0,0 +1,51 @@ +import { + PostgrestError, + PostgrestMaybeSingleResponse, + PostgrestResponse, + PostgrestSingleResponse, +} from '@supabase/postgrest-js'; +import { AnyPostgrestResponse } from '@supabase-cache-helpers/postgrest-core'; +import { FetchQueryOptions, QueryClient } from '@tanstack/vue-query'; + +import { buildQueryOpts } from './build-query-opts'; + +function fetchQuery( + queryClient: QueryClient, + query: PromiseLike>, + config?: Omit< + FetchQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): Promise>; +function fetchQuery( + queryClient: QueryClient, + query: PromiseLike>, + config?: Omit< + FetchQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): Promise>; +function fetchQuery( + queryClient: QueryClient, + query: PromiseLike>, + config?: Omit< + FetchQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): Promise>; + +async function fetchQuery( + queryClient: QueryClient, + query: PromiseLike>, + config?: Omit< + FetchQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): Promise> { + return await queryClient.fetchQuery< + AnyPostgrestResponse, + PostgrestError + >(buildQueryOpts(query, config)); +} + +export { fetchQuery }; diff --git a/packages/postgrest-vue-query/src/query/index.ts b/packages/postgrest-vue-query/src/query/index.ts new file mode 100644 index 00000000..0870aa48 --- /dev/null +++ b/packages/postgrest-vue-query/src/query/index.ts @@ -0,0 +1,4 @@ +export * from './build-query-opts'; +export * from './fetch'; +export * from './prefetch'; +export * from './use-query'; diff --git a/packages/postgrest-vue-query/src/query/prefetch.ts b/packages/postgrest-vue-query/src/query/prefetch.ts new file mode 100644 index 00000000..ab212676 --- /dev/null +++ b/packages/postgrest-vue-query/src/query/prefetch.ts @@ -0,0 +1,76 @@ +import { + PostgrestError, + PostgrestMaybeSingleResponse, + PostgrestResponse, + PostgrestSingleResponse, +} from '@supabase/postgrest-js'; +import { + AnyPostgrestResponse, + isPostgrestBuilder, +} from '@supabase-cache-helpers/postgrest-core'; +import { FetchQueryOptions, QueryClient } from '@tanstack/vue-query'; + +import { buildQueryOpts } from './build-query-opts'; +import { encode } from '../lib'; + +function prefetchQuery( + queryClient: QueryClient, + query: PromiseLike>, + config?: Omit< + FetchQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): Promise; +function prefetchQuery( + queryClient: QueryClient, + query: PromiseLike>, + config?: Omit< + FetchQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): Promise; +function prefetchQuery( + queryClient: QueryClient, + query: PromiseLike>, + config?: Omit< + FetchQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): Promise; + +async function prefetchQuery( + queryClient: QueryClient, + query: PromiseLike>, + config?: Omit< + FetchQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +) { + await queryClient.prefetchQuery, PostgrestError>( + buildQueryOpts(query, config), + ); +} + +function fetchQueryInitialData( + query: PromiseLike>, +): Promise<[string[], PostgrestSingleResponse]>; + +function fetchQueryInitialData( + query: PromiseLike>, +): Promise<[string[], PostgrestMaybeSingleResponse]>; + +function fetchQueryInitialData( + query: PromiseLike>, +): Promise<[string[], PostgrestResponse]>; + +async function fetchQueryInitialData( + query: PromiseLike>, +): Promise<[string[], AnyPostgrestResponse]> { + if (!isPostgrestBuilder(query)) { + throw new Error('Query is not a PostgrestBuilder'); + } + + return [encode(query, false), await query.throwOnError()]; +} + +export { prefetchQuery, fetchQueryInitialData }; diff --git a/packages/postgrest-vue-query/src/query/use-query.ts b/packages/postgrest-vue-query/src/query/use-query.ts new file mode 100644 index 00000000..6b1485d0 --- /dev/null +++ b/packages/postgrest-vue-query/src/query/use-query.ts @@ -0,0 +1,142 @@ +import { + PostgrestError, + PostgrestResponse, + PostgrestSingleResponse, + PostgrestMaybeSingleResponse, +} from '@supabase/postgrest-js'; +import { AnyPostgrestResponse } from '@supabase-cache-helpers/postgrest-core'; +import { + useQuery as useVueQuery, + UseQueryResult as UseVueQueryResult, + UseQueryOptions as UseVueQueryOptions, +} from '@tanstack/vue-query'; + +import { buildQueryOpts } from './build-query-opts'; + +/** + * Represents the return value of the `useQuery` hook when `query` is expected to return + * a single row. + */ +export type UseQuerySingleReturn = Omit< + UseVueQueryResult['data'], PostgrestError>, + 'refetch' +> & + Pick< + UseVueQueryResult, PostgrestError>, + 'refetch' + > & + Pick, 'count'>; + +/** + * Represents the return value of the `useQuery` hook when `query` is expected to return + * either a single row or an empty response. + */ +export type UseQueryMaybeSingleReturn = Omit< + UseVueQueryResult< + PostgrestMaybeSingleResponse['data'], + PostgrestError + >, + 'refetch' +> & + Pick< + UseVueQueryResult, PostgrestError>, + 'refetch' + > & + Pick, 'count'>; + +/** + * Represents the return value of the `useQuery` hook when `query` is expected to return + * one or more rows. + */ +export type UseQueryReturn = Omit< + UseVueQueryResult['data'], PostgrestError>, + 'refetch' +> & + Pick< + UseVueQueryResult, PostgrestError>, + 'refetch' + > & + Pick, 'count'>; + +/** + * Represents the return value of the `useQuery` hook when the type of the query response + * is not known. + */ +export type UseQueryAnyReturn = Omit< + UseVueQueryResult['data'], PostgrestError>, + 'refetch' +> & + Pick< + UseVueQueryResult, PostgrestError>, + 'refetch' + > & + Pick, 'count'>; + +/** + * Vue hook to execute a PostgREST query and return a single item response. + * + * @param {PromiseLike>} query A promise that resolves to a PostgREST single item response. + * @param {Omit, PostgrestError>, 'queryKey' | 'queryFn'>} [config] The Vue Query options. + * @returns {UseQuerySingleReturn} The hook result containing the single item response data. + */ +function useQuery( + query: PromiseLike>, + config?: Omit< + UseVueQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): UseQuerySingleReturn; +/** + * Vue hook to execute a PostgREST query and return a maybe single item response. + * + * @param {PromiseLike>} query A promise that resolves to a PostgREST maybe single item response. + * @param {Omit, PostgrestError>, 'queryKey' | 'queryFn'>} [config] The Vue Query options. + * @returns {UseQueryMaybeSingleReturn} The hook result containing the maybe single item response data. + */ +function useQuery( + query: PromiseLike>, + config?: Omit< + UseVueQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): UseQueryMaybeSingleReturn; +/** + * Vue hook to execute a PostgREST query. + * + * @template Result The expected response data type. + * @param {PromiseLike>} query A promise that resolves to a PostgREST response. + * @param {Omit, PostgrestError>, 'queryKey' | 'queryFn'>} [config] The Vue Query options. + * @returns {UseQueryReturn} The hook result containing the response data. + */ +function useQuery( + query: PromiseLike>, + config?: Omit< + UseVueQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): UseQueryReturn; + +/** + * Vue hook to execute a PostgREST query. + * + * @template Result The expected response data type. + * @param {PromiseLike>} query A promise that resolves to a PostgREST response of any kind. + * @param {Omit, PostgrestError>, 'queryKey' | 'queryFn'>} [config] The Vue Query options. + * @returns {UseQueryAnyReturn} The hook result containing the response data. + */ +function useQuery( + query: PromiseLike>, + config?: Omit< + UseVueQueryOptions, PostgrestError>, + 'queryKey' | 'queryFn' + >, +): UseQueryAnyReturn { + const { data, ...rest } = useVueQuery< + AnyPostgrestResponse, + PostgrestError + >(buildQueryOpts(query, config)); + + return { data: data?.data, count: data?.count ?? null, ...rest }; +} + +export { useQuery }; diff --git a/packages/postgrest-vue-query/src/subscribe/index.ts b/packages/postgrest-vue-query/src/subscribe/index.ts new file mode 100644 index 00000000..2289783c --- /dev/null +++ b/packages/postgrest-vue-query/src/subscribe/index.ts @@ -0,0 +1,2 @@ +export * from './use-subscription-query'; +export * from './use-subscription'; diff --git a/packages/postgrest-vue-query/src/subscribe/use-subscription-query.ts b/packages/postgrest-vue-query/src/subscribe/use-subscription-query.ts new file mode 100644 index 00000000..62cac68e --- /dev/null +++ b/packages/postgrest-vue-query/src/subscribe/use-subscription-query.ts @@ -0,0 +1,172 @@ +import { GetResult } from '@supabase/postgrest-js/dist/module/select-query-parser'; +import { + GenericSchema, + GenericTable, +} from '@supabase/postgrest-js/dist/module/types'; +import { + RealtimeChannel, + RealtimePostgresChangesFilter, + RealtimePostgresChangesPayload, + REALTIME_LISTEN_TYPES, + REALTIME_POSTGRES_CHANGES_LISTEN_EVENT, + SupabaseClient, +} from '@supabase/supabase-js'; +import { + buildNormalizedQuery, + normalizeResponse, + RevalidateOpts, +} from '@supabase-cache-helpers/postgrest-core'; +import { MutationOptions as VueQueryMutatorOptions } from '@tanstack/vue-query'; +import { watchEffect, ref } from 'vue'; + +import { useDeleteItem, useUpsertItem } from '../cache'; +import { useQueriesForTableLoader } from '../lib'; + +/** + * Options for `useSubscriptionQuery` hook + */ +export type UseSubscriptionQueryOpts< + S extends GenericSchema, + T extends GenericTable, + RelationName, + Relatsonships, + Q extends string = '*', + R = GetResult< + S, + T['Row'], + RelationName, + Relatsonships, + Q extends '*' ? '*' : Q + >, +> = RevalidateOpts & + VueQueryMutatorOptions & { + /** + * A callback that will be called whenever a realtime event occurs for the given channel. + * The callback will receive the event payload with an additional "data" property, which will be + * the affected row of the event (or a modified version of it, if a select query is provided). + */ + callback?: ( + event: RealtimePostgresChangesPayload & { data: T['Row'] | R }, + ) => void | Promise; + }; + +/** + * A hook for subscribing to realtime Postgres events on a given channel. + * + * The subscription will automatically update the cache for the specified table in response + * to incoming Postgres events, and optionally run a user-provided callback function with the + * event and the updated data. + * + * This hook works by creating a Supabase Realtime channel for the specified table and + * subscribing to Postgres changes on that channel. When an event is received, the hook + * fetches the updated data from the database (using a `select` query generated from the cache + * configuration), and then updates the cache accordingly. + * + * @param client - The Supabase client instance. + * @param channelName - The name of the channel to subscribe to. + * @param filter - The filter object to use when listening for changes. + * @param primaryKeys - An array of the primary keys for the table being listened to. + * @param query - An optional PostgREST query to use when selecting data for an event. + * @param opts - Additional options to pass to the hook. + * @returns An object containing the RealtimeChannel and the current status of the subscription. + */ +function useSubscriptionQuery< + S extends GenericSchema, + T extends GenericTable, + RelationName, + Relationships, + Q extends string = '*', + R = GetResult< + S, + T['Row'], + RelationName, + Relationships, + Q extends '*' ? '*' : Q + >, +>( + client: SupabaseClient | null, + channelName: string, + filter: Omit< + RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL}`>, + 'table' + > & { table: string }, + primaryKeys: (keyof T['Row'])[], + query?: Q extends '*' ? "'*' is not allowed" : Q | null, + opts?: UseSubscriptionQueryOpts, +) { + const statusRef = ref(); + const channelRef = ref(); + const queriesForTable = useQueriesForTableLoader(filter.table); + const deleteItem = useDeleteItem({ + ...opts, + primaryKeys, + table: filter.table, + schema: filter.schema, + }); + const upsertItem = useUpsertItem({ + ...opts, + primaryKeys, + table: filter.table, + schema: filter.schema, + }); + + watchEffect((onCleanup) => { + if (!client) return; + + const c = client + .channel(channelName) + .on( + REALTIME_LISTEN_TYPES.POSTGRES_CHANGES, + filter, + async (payload) => { + let data: T['Row'] | R = payload.new ?? payload.old; + const selectQuery = buildNormalizedQuery({ queriesForTable, query }); + if ( + payload.eventType !== + REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE && + selectQuery + ) { + const qb = client + .from(payload.table) + .select(selectQuery.selectQuery); + for (const pk of primaryKeys) { + qb.eq(pk.toString(), data[pk]); + } + const res = await qb.single(); + if (res.data) { + data = normalizeResponse(selectQuery.groupedPaths, res.data) as R; + } + } + + if ( + payload.eventType === + REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT || + payload.eventType === REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE + ) { + await upsertItem(data as Record); + } else if ( + payload.eventType === REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE + ) { + await deleteItem(data as Record); + } + if (opts?.callback) { + opts.callback({ + ...payload, + data, + }); + } + }, + ) + .subscribe((status: string) => (statusRef.value = status)); + + channelRef.value = c; + + onCleanup(() => { + if (c) c.unsubscribe(); + }); + }); + + return { channelRef, statusRef }; +} + +export { useSubscriptionQuery }; diff --git a/packages/postgrest-vue-query/src/subscribe/use-subscription.ts b/packages/postgrest-vue-query/src/subscribe/use-subscription.ts new file mode 100644 index 00000000..9cf68050 --- /dev/null +++ b/packages/postgrest-vue-query/src/subscribe/use-subscription.ts @@ -0,0 +1,98 @@ +import { GenericTable } from '@supabase/postgrest-js/dist/module/types'; +import { + RealtimePostgresChangesFilter, + RealtimePostgresChangesPayload, + REALTIME_LISTEN_TYPES, + REALTIME_POSTGRES_CHANGES_LISTEN_EVENT, + SupabaseClient, +} from '@supabase/supabase-js'; +import { RevalidateOpts } from '@supabase-cache-helpers/postgrest-core'; +import { MutationOptions as VueQueryMutatorOptions } from '@tanstack/vue-query'; +import { watchEffect, ref } from 'vue'; + +import { useDeleteItem, useUpsertItem } from '../cache'; + +/** + * Options for the `useSubscription` hook. + */ +export type UseSubscriptionOpts = RevalidateOpts< + T['Row'] +> & + VueQueryMutatorOptions & { + callback?: ( + event: RealtimePostgresChangesPayload, + ) => void | Promise; + }; + +/** + * Hook that sets up a real-time subscription to a Postgres database table. + * + * @param channel - The real-time channel to subscribe to. + * @param filter - A filter that specifies the table and conditions for the subscription. + * @param primaryKeys - An array of primary key column names for the table. + * @param opts - Options for the mutation function used to upsert or delete rows in the cache. + * + * @returns An object containing the current status of the subscription. + */ +function useSubscription( + client: SupabaseClient | null, + channelName: string, + filter: Omit< + RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL}`>, + 'table' + > & { table: string }, + primaryKeys: (keyof T['Row'])[], + opts?: UseSubscriptionOpts, +) { + const statusRef = ref(); + const deleteItem = useDeleteItem({ + ...opts, + primaryKeys, + table: filter.table, + schema: filter.schema, + }); + const upsertItem = useUpsertItem({ + ...opts, + primaryKeys, + table: filter.table, + schema: filter.schema, + }); + + watchEffect(() => { + if (!client) return; + + const c = client + .channel(channelName) + .on( + REALTIME_LISTEN_TYPES.POSTGRES_CHANGES, + filter, + async (payload) => { + if ( + payload.eventType === + REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT || + payload.eventType === REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE + ) { + await upsertItem(payload.new); + } else if ( + payload.eventType === REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE + ) { + await deleteItem(payload.old); + } + if (opts?.callback) { + opts.callback({ + ...payload, + }); + } + }, + ) + .subscribe((status: string) => (statusRef.value = status)); + + return () => { + if (c) c.unsubscribe(); + }; + }); + + return { status }; +} + +export { useSubscription }; diff --git a/packages/postgrest-vue-query/tsconfig.json b/packages/postgrest-vue-query/tsconfig.json new file mode 100644 index 00000000..5a38a080 --- /dev/null +++ b/packages/postgrest-vue-query/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@supabase-cache-helpers/tsconfig/base.json", + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/postgrest-vue-query/tsup.config.ts b/packages/postgrest-vue-query/tsup.config.ts new file mode 100644 index 00000000..7cfe8a38 --- /dev/null +++ b/packages/postgrest-vue-query/tsup.config.ts @@ -0,0 +1,15 @@ +import type { Options } from 'tsup'; + +export const tsup: Options = { + dts: true, + entryPoints: ['src/index.ts'], + external: ['vue', /^@supabase\//], + format: ['cjs', 'esm'], + // inject: ['src/react-shim.js'], + // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default! + legacyOutput: false, + sourcemap: true, + splitting: false, + bundle: true, + clean: true, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5891c99a..21731fed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -715,6 +715,73 @@ importers: specifier: 5.4.2 version: 5.4.2 + packages/postgrest-vue-query: + dependencies: + '@supabase-cache-helpers/postgrest-core': + specifier: workspace:* + version: link:../postgrest-core + '@tanstack/vue-query': + specifier: ^5.28.13 + version: 5.28.13(vue@3.4.21) + flat: + specifier: 5.0.2 + version: 5.0.2 + devDependencies: + '@supabase-cache-helpers/eslint-config-custom': + specifier: workspace:* + version: link:../eslint-config-custom + '@supabase-cache-helpers/jest-presets': + specifier: workspace:* + version: link:../jest-presets + '@supabase-cache-helpers/prettier-config': + specifier: workspace:* + version: link:../prettier-config + '@supabase-cache-helpers/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@supabase/postgrest-js': + specifier: 1.9.0 + version: 1.9.0 + '@supabase/supabase-js': + specifier: 2.38.5 + version: 2.38.5 + '@testing-library/jest-dom': + specifier: 6.4.0 + version: 6.4.0(@types/jest@29.5.0)(jest@29.7.0) + '@testing-library/vue': + specifier: 8.0.3 + version: 8.0.3(vue@3.4.21) + '@types/flat': + specifier: 5.0.2 + version: 5.0.2 + '@types/jest': + specifier: 29.5.0 + version: 29.5.0 + dotenv: + specifier: 16.4.0 + version: 16.4.0 + eslint: + specifier: 8.54.0 + version: 8.54.0 + jest: + specifier: 29.7.0 + version: 29.7.0 + jest-environment-jsdom: + specifier: 29.7.0 + version: 29.7.0 + ts-jest: + specifier: 29.1.0 + version: 29.1.0(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2) + tsup: + specifier: 8.0.0 + version: 8.0.0(typescript@5.4.2) + typescript: + specifier: 5.4.2 + version: 5.4.2 + vue: + specifier: 3.4.21 + version: 3.4.21(typescript@5.4.2) + packages/prettier-config: devDependencies: prettier: @@ -1238,6 +1305,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.0): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: @@ -1246,6 +1321,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.0): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: @@ -1254,6 +1337,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.0): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -1262,6 +1353,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.0): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -1270,6 +1369,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.0): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5): resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} engines: {node: '>=6.9.0'} @@ -1287,6 +1394,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.0): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: @@ -1295,6 +1410,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.0): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: @@ -1303,6 +1426,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.0): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: @@ -1311,6 +1442,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.0): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: @@ -1319,6 +1458,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.0): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: @@ -1327,6 +1474,14 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.0): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} @@ -1336,6 +1491,15 @@ packages: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.0): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.0 + '@babel/helper-plugin-utils': 7.18.9 + /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.5): resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} engines: {node: '>=6.9.0'} @@ -1973,7 +2137,6 @@ packages: strip-ansi-cjs: /strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: false /@istanbuljs/load-nyc-config@1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} @@ -2638,11 +2801,14 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + /@one-ini/wasm@0.1.1: + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + dev: true + /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} requiresBuild: true - dev: false optional: true /@pkgr/utils@2.3.1: @@ -4112,10 +4278,21 @@ packages: tslib: 2.5.0 dev: false + /@tanstack/match-sorter-utils@8.15.1: + resolution: {integrity: sha512-PnVV3d2poenUM31ZbZi/yXkBu3J7kd5k2u51CGwwNojag451AjTH9N6n41yjXz2fpLeewleyLBmNS6+HcGDlXw==} + engines: {node: '>=12'} + dependencies: + remove-accents: 0.5.0 + dev: false + /@tanstack/query-core@5.0.0: resolution: {integrity: sha512-Y1BpiA6BblJd/UlVqxEVeAG7IACn568YJuTTItAiecBI7En+33g780kg+/8lhgl+BzcUPN7o+NjBrSRGJoemyQ==} dev: false + /@tanstack/query-core@5.28.13: + resolution: {integrity: sha512-C3+CCOcza+mrZ7LglQbjeYEOTEC3LV0VN0eYaIN6GvqAZ8Foegdgch7n6QYPtT4FuLae5ALy+m+ZMEKpD6tMCQ==} + dev: false + /@tanstack/react-query@5.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-diQoC8FNBcO5Uf5yuaJlXthTtbO1xM8kzOX+pSBUMT9n/cqQ/u1wJGCtukvhDWA+6j07WmIj4bfqNbd2KOB6jQ==} peerDependencies: @@ -4133,6 +4310,22 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@tanstack/vue-query@5.28.13(vue@3.4.21): + resolution: {integrity: sha512-MLSODlf0kYXt3YQgF8xTrF8pZ30UpIlfLNbydXsWrsE7bZaYdRnz3geQTQHD722r+O/GdNB55INesAoLZaPL5A==} + peerDependencies: + '@vue/composition-api': ^1.1.2 + vue: ^2.6.0 || ^3.3.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + '@tanstack/match-sorter-utils': 8.15.1 + '@tanstack/query-core': 5.28.13 + '@vue/devtools-api': 6.6.1 + vue: 3.4.21(typescript@5.4.2) + vue-demi: 0.14.7(vue@3.4.21) + dev: false + /@testing-library/dom@9.3.3: resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} engines: {node: '>=14'} @@ -4194,6 +4387,22 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@testing-library/vue@8.0.3(vue@3.4.21): + resolution: {integrity: sha512-wSsbNlZ69ZFQgVlHMtc/ZC/g9BHO7MhyDrd4nHyfEubtMr3kToN/w4/BsSBknGIF8w9UmPbsgbIuq/CbdBHzCA==} + engines: {node: '>=14'} + peerDependencies: + '@vue/compiler-sfc': '>= 3' + vue: '>= 3' + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + dependencies: + '@babel/runtime': 7.23.5 + '@testing-library/dom': 9.3.3 + '@vue/test-utils': 2.4.5 + vue: 3.4.21(typescript@5.4.2) + dev: true + /@theguild/remark-mermaid@0.0.5(react@18.2.0): resolution: {integrity: sha512-e+ZIyJkEv9jabI4m7q29wZtZv+2iwPGsXJ2d46Zi7e+QcFudiyuqhLhHG/3gX3ZEB+hxTch+fpItyMS8jwbIcw==} peerDependencies: @@ -4688,10 +4897,90 @@ packages: server-only: 0.0.1 dev: false + /@vue/compiler-core@3.4.21: + resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==} + dependencies: + '@babel/parser': 7.24.0 + '@vue/shared': 3.4.21 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.0.2 + + /@vue/compiler-dom@3.4.21: + resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} + dependencies: + '@vue/compiler-core': 3.4.21 + '@vue/shared': 3.4.21 + + /@vue/compiler-sfc@3.4.21: + resolution: {integrity: sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==} + dependencies: + '@babel/parser': 7.24.0 + '@vue/compiler-core': 3.4.21 + '@vue/compiler-dom': 3.4.21 + '@vue/compiler-ssr': 3.4.21 + '@vue/shared': 3.4.21 + estree-walker: 2.0.2 + magic-string: 0.30.9 + postcss: 8.4.38 + source-map-js: 1.0.2 + + /@vue/compiler-ssr@3.4.21: + resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==} + dependencies: + '@vue/compiler-dom': 3.4.21 + '@vue/shared': 3.4.21 + + /@vue/devtools-api@6.6.1: + resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==} + dev: false + + /@vue/reactivity@3.4.21: + resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==} + dependencies: + '@vue/shared': 3.4.21 + + /@vue/runtime-core@3.4.21: + resolution: {integrity: sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==} + dependencies: + '@vue/reactivity': 3.4.21 + '@vue/shared': 3.4.21 + + /@vue/runtime-dom@3.4.21: + resolution: {integrity: sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==} + dependencies: + '@vue/runtime-core': 3.4.21 + '@vue/shared': 3.4.21 + csstype: 3.1.3 + + /@vue/server-renderer@3.4.21(vue@3.4.21): + resolution: {integrity: sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==} + peerDependencies: + vue: 3.4.21 + dependencies: + '@vue/compiler-ssr': 3.4.21 + '@vue/shared': 3.4.21 + vue: 3.4.21(typescript@5.4.2) + + /@vue/shared@3.4.21: + resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==} + + /@vue/test-utils@2.4.5: + resolution: {integrity: sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==} + dependencies: + js-beautify: 1.15.1 + vue-component-type-helpers: 2.0.10 + dev: true + /abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: true + /abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: @@ -4780,7 +5069,6 @@ packages: /ansi-regex@6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} - dev: false /ansi-sequence-parser@1.1.0: resolution: {integrity: sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==} @@ -4805,7 +5093,6 @@ packages: /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - dev: false /any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -4974,17 +5261,17 @@ packages: dependencies: deep-equal: 2.2.0 - /babel-jest@29.7.0(@babel/core@7.23.5): + /babel-jest@29.7.0(@babel/core@7.24.0): resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.1.19 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.23.5) + babel-preset-jest: 29.6.3(@babel/core@7.24.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -5031,15 +5318,34 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.5) - /babel-preset-jest@29.6.3(@babel/core@7.23.5): + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.0): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.0) + + /babel-preset-jest@29.6.3(@babel/core@7.24.0): resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.0 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.0) /bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -5091,7 +5397,6 @@ packages: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 - dev: false /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} @@ -5450,6 +5755,11 @@ packages: resolution: {integrity: sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==} dev: false + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: true + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -5471,6 +5781,13 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: true + /convert-source-map@1.8.0: resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} dependencies: @@ -5559,6 +5876,9 @@ packages: /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + /csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} dev: true @@ -6132,7 +6452,17 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: false + + /editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.5.4 + dev: true /electron-to-chromium@1.4.325: resolution: {integrity: sha512-K1C03NT4I7BuzsRdCU5RWkgZxtswnKDYM6/eMhkEXqKu4e5T+ck610x3FPzu1y7HVFSiQKZqP16gnJzPpji1TQ==} @@ -6183,6 +6513,10 @@ packages: resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} engines: {node: '>=0.12'} + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -6895,6 +7229,9 @@ packages: '@types/unist': 2.0.6 dev: false + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + /estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: @@ -7089,7 +7426,6 @@ packages: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 - dev: false /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} @@ -7256,7 +7592,6 @@ packages: minimatch: 9.0.3 minipass: 7.0.4 path-scurry: 1.10.1 - dev: false /glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} @@ -7659,7 +7994,6 @@ packages: /ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: false /inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} @@ -8026,7 +8360,6 @@ packages: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 - dev: false /jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} @@ -8103,11 +8436,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 20.10.1 - babel-jest: 29.7.0(@babel/core@7.23.5) + babel-jest: 29.7.0(@babel/core@7.24.0) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -8448,6 +8781,23 @@ packages: engines: {node: '>=10'} dev: true + /js-beautify@1.15.1: + resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} + engines: {node: '>=14'} + hasBin: true + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.3.10 + js-cookie: 3.0.5 + nopt: 7.2.0 + dev: true + + /js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + dev: true + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -8689,7 +9039,6 @@ packages: /lru-cache@10.1.0: resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} engines: {node: 14 || >=16.14} - dev: false /lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -8721,6 +9070,12 @@ packages: hasBin: true dev: true + /magic-string@0.30.9: + resolution: {integrity: sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -9435,12 +9790,18 @@ packages: dependencies: brace-expansion: 1.1.11 + /minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 - dev: false /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} @@ -9469,7 +9830,6 @@ packages: /minipass@7.0.4: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} engines: {node: '>=16 || 14 >=14.17'} - dev: false /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} @@ -9523,6 +9883,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + /napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} dev: false @@ -9771,6 +10136,14 @@ packages: resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} dev: false + /nopt@7.2.0: + resolution: {integrity: sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + abbrev: 2.0.0 + dev: true + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -10044,7 +10417,6 @@ packages: dependencies: lru-cache: 10.1.0 minipass: 7.0.4 - dev: false /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -10173,6 +10545,14 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + /prebuild-install@7.1.1: resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} engines: {node: '>=10'} @@ -10265,6 +10645,10 @@ packages: resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==} dev: false + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: true + /protocols@2.0.1: resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} dev: false @@ -10678,6 +11062,10 @@ packages: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} dev: false + /remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + dev: false + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -10981,6 +11369,10 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: @@ -11084,7 +11476,6 @@ packages: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 strip-ansi: 7.1.0 - dev: false /string.prototype.matchall@4.0.8: resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} @@ -11144,7 +11535,6 @@ packages: engines: {node: '>=12'} dependencies: ansi-regex: 6.0.1 - dev: false /strip-bom-string@1.0.0: resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} @@ -12185,6 +12575,40 @@ packages: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} dev: false + /vue-component-type-helpers@2.0.10: + resolution: {integrity: sha512-FC5fKJjDks3Ue/KRSYBdsiCaZa0kUPQfs8yQpb8W9mlO6BenV8G1z58xobeRMzevnmEcDa09LLwuXDwb4f6NMQ==} + dev: true + + /vue-demi@0.14.7(vue@3.4.21): + resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.4.21(typescript@5.4.2) + dev: false + + /vue@3.4.21(typescript@5.4.2): + resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@vue/compiler-dom': 3.4.21 + '@vue/compiler-sfc': 3.4.21 + '@vue/runtime-dom': 3.4.21 + '@vue/server-renderer': 3.4.21(vue@3.4.21) + '@vue/shared': 3.4.21 + typescript: 5.4.2 + /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} @@ -12363,7 +12787,6 @@ packages: ansi-styles: 6.2.1 string-width: 5.1.2 strip-ansi: 7.1.0 - dev: false /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} From 9bcc6b700e90d2bab58827a71d09078c370dc3f8 Mon Sep 17 00:00:00 2001 From: Christian Pannwitz Date: Wed, 10 Apr 2024 21:52:03 +0200 Subject: [PATCH 2/5] fix(postgrest-vue-query): fix mutation hook types --- .../src/mutate/use-delete-many-mutation.ts | 2 +- .../src/mutate/use-delete-mutation.ts | 2 +- .../src/mutate/use-insert-mutation.ts | 2 +- .../src/mutate/use-update-mutation.ts | 2 +- packages/postgrest-vue-query/src/query/use-query.ts | 10 ++++++++-- .../src/subscribe/use-subscription.ts | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts index d3f25c94..a7aac9df 100644 --- a/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts +++ b/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts @@ -47,7 +47,7 @@ function useDeleteManyMutation< }); return useMutation({ - mutationFn: async (input) => { + mutationFn: async (input: T['Row'][]) => { const result = await buildDeleteFetcher( qb, primaryKeys, diff --git a/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts index ab86a8b0..f8bbf34b 100644 --- a/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts +++ b/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts @@ -47,7 +47,7 @@ function useDeleteMutation< }); return useMutation({ - mutationFn: async (input) => { + mutationFn: async (input: T['Row']) => { const r = await buildDeleteFetcher( qb, primaryKeys, diff --git a/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts index 6f3f2013..f70e9baa 100644 --- a/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts +++ b/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts @@ -48,7 +48,7 @@ function useInsertMutation< }); return useMutation({ - mutationFn: async (input) => { + mutationFn: async (input: T['Insert'][]) => { const result = await buildInsertFetcher( qb, { diff --git a/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts index f6e107c3..cc3e7c84 100644 --- a/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts +++ b/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts @@ -47,7 +47,7 @@ function useUpdateMutation< }); return useMutation({ - mutationFn: async (input) => { + mutationFn: async (input: T['Update']) => { const result = await buildUpdateFetcher( qb, primaryKeys, diff --git a/packages/postgrest-vue-query/src/query/use-query.ts b/packages/postgrest-vue-query/src/query/use-query.ts index 6b1485d0..adc8c85e 100644 --- a/packages/postgrest-vue-query/src/query/use-query.ts +++ b/packages/postgrest-vue-query/src/query/use-query.ts @@ -7,7 +7,7 @@ import { import { AnyPostgrestResponse } from '@supabase-cache-helpers/postgrest-core'; import { useQuery as useVueQuery, - UseQueryResult as UseVueQueryResult, + UseQueryReturnType as UseVueQueryResult, UseQueryOptions as UseVueQueryOptions, } from '@tanstack/vue-query'; @@ -136,7 +136,13 @@ function useQuery( PostgrestError >(buildQueryOpts(query, config)); - return { data: data?.data, count: data?.count ?? null, ...rest }; + // TODO: data: data.value || Type 'AnyPostgrestResponse | undefined' is not assignable to type 'Ref | Ref' + // TODO: data: data.value?.data || Type 'Result | Result[] | null | undefined' is not assignable to type 'Ref | Ref'. + return { + data: data.value?.data, + count: data.value?.count ?? null, + ...rest, + }; } export { useQuery }; diff --git a/packages/postgrest-vue-query/src/subscribe/use-subscription.ts b/packages/postgrest-vue-query/src/subscribe/use-subscription.ts index 9cf68050..d4c6ce63 100644 --- a/packages/postgrest-vue-query/src/subscribe/use-subscription.ts +++ b/packages/postgrest-vue-query/src/subscribe/use-subscription.ts @@ -92,7 +92,7 @@ function useSubscription( }; }); - return { status }; + return { status: statusRef }; } export { useSubscription }; From d6dbc782b6054322a73d3061733819d8f99ded25 Mon Sep 17 00:00:00 2001 From: Christian Pannwitz Date: Thu, 11 Apr 2024 08:58:31 +0200 Subject: [PATCH 3/5] feature: add storage-vue-query --- packages/postgrest-vue-query/tsconfig.json | 7 +- packages/storage-vue-query/.eslintrc.json | 4 + packages/storage-vue-query/CHANGELOG.md | 77 ++++++++++++++ packages/storage-vue-query/README.md | 29 ++++++ .../__tests__/__fixtures__/1.jpg | Bin 0 -> 12268 bytes .../__tests__/__fixtures__/2.jpg | Bin 0 -> 10688 bytes .../__tests__/__fixtures__/3.jpg | Bin 0 -> 10490 bytes .../__tests__/__fixtures__/4.jpg | Bin 0 -> 10642 bytes .../__tests__/lib/decode.spec.ts | 7 ++ .../__tests__/lib/key.spec.ts | 11 ++ .../mutate/use-remove-directory.spec.tsx | 53 ++++++++++ .../mutate/use-remove-files.spec.tsx | 56 ++++++++++ .../__tests__/mutate/use-upload.spec.tsx | 64 ++++++++++++ .../query/use-directory-urls.spec.tsx | 58 +++++++++++ .../__tests__/query/use-directory.spec.tsx | 56 ++++++++++ .../__tests__/query/use-file-url.spec.tsx | 48 +++++++++ .../storage-vue-query/__tests__/utils.tsx | 65 ++++++++++++ packages/storage-vue-query/package.json | 80 ++++++++++++++ .../storage-vue-query/prettier.config.cjs | 1 + packages/storage-vue-query/src/index.ts | 3 + .../storage-vue-query/src/lib/constants.ts | 2 + packages/storage-vue-query/src/lib/decode.ts | 12 +++ packages/storage-vue-query/src/lib/encode.ts | 10 ++ .../src/lib/get-bucket-id.ts | 4 + packages/storage-vue-query/src/lib/index.ts | 7 ++ packages/storage-vue-query/src/lib/key.ts | 16 +++ packages/storage-vue-query/src/lib/truthy.ts | 5 + packages/storage-vue-query/src/lib/types.ts | 3 + .../storage-vue-query/src/mutate/index.ts | 3 + .../src/mutate/use-remove-directory.ts | 51 +++++++++ .../src/mutate/use-remove-files.ts | 51 +++++++++ .../src/mutate/use-upload.ts | 70 +++++++++++++ packages/storage-vue-query/src/query/index.ts | 3 + .../src/query/use-directory-urls.ts | 70 +++++++++++++ .../src/query/use-directory.ts | 45 ++++++++ .../src/query/use-file-url.ts | 56 ++++++++++ packages/storage-vue-query/tsconfig.json | 8 ++ packages/storage-vue-query/tsup.config.ts | 15 +++ pnpm-lock.yaml | 98 +++++++++++++----- 39 files changed, 1121 insertions(+), 27 deletions(-) create mode 100644 packages/storage-vue-query/.eslintrc.json create mode 100644 packages/storage-vue-query/CHANGELOG.md create mode 100644 packages/storage-vue-query/README.md create mode 100644 packages/storage-vue-query/__tests__/__fixtures__/1.jpg create mode 100644 packages/storage-vue-query/__tests__/__fixtures__/2.jpg create mode 100644 packages/storage-vue-query/__tests__/__fixtures__/3.jpg create mode 100644 packages/storage-vue-query/__tests__/__fixtures__/4.jpg create mode 100644 packages/storage-vue-query/__tests__/lib/decode.spec.ts create mode 100644 packages/storage-vue-query/__tests__/lib/key.spec.ts create mode 100644 packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.tsx create mode 100644 packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.tsx create mode 100644 packages/storage-vue-query/__tests__/mutate/use-upload.spec.tsx create mode 100644 packages/storage-vue-query/__tests__/query/use-directory-urls.spec.tsx create mode 100644 packages/storage-vue-query/__tests__/query/use-directory.spec.tsx create mode 100644 packages/storage-vue-query/__tests__/query/use-file-url.spec.tsx create mode 100644 packages/storage-vue-query/__tests__/utils.tsx create mode 100644 packages/storage-vue-query/package.json create mode 100644 packages/storage-vue-query/prettier.config.cjs create mode 100644 packages/storage-vue-query/src/index.ts create mode 100644 packages/storage-vue-query/src/lib/constants.ts create mode 100644 packages/storage-vue-query/src/lib/decode.ts create mode 100644 packages/storage-vue-query/src/lib/encode.ts create mode 100644 packages/storage-vue-query/src/lib/get-bucket-id.ts create mode 100644 packages/storage-vue-query/src/lib/index.ts create mode 100644 packages/storage-vue-query/src/lib/key.ts create mode 100644 packages/storage-vue-query/src/lib/truthy.ts create mode 100644 packages/storage-vue-query/src/lib/types.ts create mode 100644 packages/storage-vue-query/src/mutate/index.ts create mode 100644 packages/storage-vue-query/src/mutate/use-remove-directory.ts create mode 100644 packages/storage-vue-query/src/mutate/use-remove-files.ts create mode 100644 packages/storage-vue-query/src/mutate/use-upload.ts create mode 100644 packages/storage-vue-query/src/query/index.ts create mode 100644 packages/storage-vue-query/src/query/use-directory-urls.ts create mode 100644 packages/storage-vue-query/src/query/use-directory.ts create mode 100644 packages/storage-vue-query/src/query/use-file-url.ts create mode 100644 packages/storage-vue-query/tsconfig.json create mode 100644 packages/storage-vue-query/tsup.config.ts diff --git a/packages/postgrest-vue-query/tsconfig.json b/packages/postgrest-vue-query/tsconfig.json index 5a38a080..c5f94dfa 100644 --- a/packages/postgrest-vue-query/tsconfig.json +++ b/packages/postgrest-vue-query/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "@supabase-cache-helpers/tsconfig/base.json", + "extends": "@supabase-cache-helpers/tsconfig/web.json", "include": ["**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], + "compilerOptions": { + "noErrorTruncation": true + } } diff --git a/packages/storage-vue-query/.eslintrc.json b/packages/storage-vue-query/.eslintrc.json new file mode 100644 index 00000000..c54a0612 --- /dev/null +++ b/packages/storage-vue-query/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": ["@supabase-cache-helpers/custom"] +} diff --git a/packages/storage-vue-query/CHANGELOG.md b/packages/storage-vue-query/CHANGELOG.md new file mode 100644 index 00000000..202549aa --- /dev/null +++ b/packages/storage-vue-query/CHANGELOG.md @@ -0,0 +1,77 @@ +# @supabase-cache-helpers/storage-vue-query + +## 1.2.1 + +### Patch Changes + +- 40a6327: fix: update typescript to 5.4.2 +- Updated dependencies [40a6327] + - @supabase-cache-helpers/storage-core@0.0.4 + +## 1.2.0 + +### Minor Changes + +- bfbb039: feat: add query opt builder fns for storage + +## 1.1.2 + +### Patch Changes + +- f2ca765: chore: upgrade supabase-js to 2.38.5 +- f2ca765: chore: upgrade storage-js to 2.5.5 +- Updated dependencies [f2ca765] +- Updated dependencies [f2ca765] + - @supabase-cache-helpers/storage-core@0.0.3 + +## 1.1.1 + +### Patch Changes + +- Updated dependencies [f9dd4e4] + - @supabase-cache-helpers/storage-core@0.0.2 + +## 1.1.0 + +### Minor Changes + +- 7a71f52: chore: add support for react-query v5 + +## 1.0.4 + +### Patch Changes + +- 6b1f00c: fix: type configuration parameter and add tests for fallbackData +- 2f1d3cb: refactor: merge internal packages into one core package per product +- Updated dependencies [2f1d3cb] + - @supabase-cache-helpers/storage-core@0.0.1 + +## 1.0.3 + +### Patch Changes + +- ad7efb0: chore: upgrade supabase to latest +- Updated dependencies [ad7efb0] + - @supabase-cache-helpers/storage-fetcher@1.0.9 + +## 1.0.2 + +### Patch Changes + +- 5acf83a: Fix types for mjs when using "moduleResolution" other then "node" (node16, nodenext, bundler) +- Updated dependencies [5acf83a] + - @supabase-cache-helpers/storage-fetcher@1.0.8 + - @supabase-cache-helpers/storage-mutate@1.0.4 + +## 1.0.1 + +### Patch Changes + +- Updated dependencies [9fd9f7e] + - @supabase-cache-helpers/storage-fetcher@1.0.7 + +## 1.0.0 + +### Major Changes + +- 43a126e: feat: initial commit diff --git a/packages/storage-vue-query/README.md b/packages/storage-vue-query/README.md new file mode 100644 index 00000000..1de45a13 --- /dev/null +++ b/packages/storage-vue-query/README.md @@ -0,0 +1,29 @@ +# Supabase Storage Vue Query + +A collection of React Query utilities for working with Supabase. + +Latest build +GitHub Stars +[![codecov](https://codecov.io/gh/psteinroe/supabase-cache-helpers/branch/main/graph/badge.svg?token=SPMWSVBRGX)](https://codecov.io/gh/psteinroe/supabase-cache-helpers) + +## Introduction + +The cache helpers bridge the gap between popular frontend cache management solutions such as [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest), and the Supabase client libraries. All features of [`postgrest-js`](https://github.com/supabase/postgrest-js), [`storage-js`](https://github.com/supabase/storage-js) and [`realtime-js`](https://github.com/supabase/realtime-js) are supported. The cache helpers parse any query into a unique and definite query key, and automatically populates your query cache with every mutation using implicit knowledge of the schema. Check out the [demo](https://supabase-cache-helpers-react-query.vercel.app/) and find out how it feels like for your users. + +## Features + +With just one single line of code, you can simplify the logic of **fetching, subscribing to updates, and mutating data as well as storage objects** in your project, and have all the amazing features of [SWR](https://swr.vercel.app) or [React Query](https://tanstack.com/query/latest) out-of-the-box. + +- **Seamless** integration with [SWR](https://swr.vercel.app) and [React Query](https://tanstack.com/query/latest) +- **Automatic** cache key generation +- Easy **Pagination** and **Infinite Scroll** queries +- **Insert**, **update**, **upsert** and **delete** mutations +- **Auto-populate** cache after mutations and subscriptions +- **Auto-expand** mutation queries based on existing cache data to keep app up-to-date +- One-liner to upload, download and remove **Supabase Storage** objects + +And a lot [more](https://supabase-cache-helpers.vercel.app). + +--- + +**View full documentation and examples on [supabase-cache-helpers.vercel.app](https://supabase-cache-helpers.vercel.app).** diff --git a/packages/storage-vue-query/__tests__/__fixtures__/1.jpg b/packages/storage-vue-query/__tests__/__fixtures__/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a034ac4dc0267c4913fd0079329b3ea7d8bdd126 GIT binary patch literal 12268 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

^2&*@{hl{F+U=@ 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 literal 0 HcmV?d00001 diff --git a/packages/storage-vue-query/__tests__/__fixtures__/4.jpg b/packages/storage-vue-query/__tests__/__fixtures__/4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..956dfc8263a0026beb256a8e8b729329e8a9da2f GIT binary patch literal 10642 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 literal 0 HcmV?d00001 diff --git a/packages/storage-vue-query/__tests__/lib/decode.spec.ts b/packages/storage-vue-query/__tests__/lib/decode.spec.ts new file mode 100644 index 00000000..9f18c385 --- /dev/null +++ b/packages/storage-vue-query/__tests__/lib/decode.spec.ts @@ -0,0 +1,7 @@ +import { decode } from '../../src'; + +describe('decode', () => { + it('should return null for invalid key', () => { + expect(decode(['some', 'unrelated', 'key'])).toEqual(null); + }); +}); diff --git a/packages/storage-vue-query/__tests__/lib/key.spec.ts b/packages/storage-vue-query/__tests__/lib/key.spec.ts new file mode 100644 index 00000000..898d28d2 --- /dev/null +++ b/packages/storage-vue-query/__tests__/lib/key.spec.ts @@ -0,0 +1,11 @@ +import { assertStorageKeyInput } from '../../src/lib'; + +describe('key', () => { + describe('assertStorageKeyInput', () => { + it('should throw for invalid key', () => { + expect(() => assertStorageKeyInput(['some', 'unrelated', 'key'])).toThrow( + 'Invalid key', + ); + }); + }); +}); diff --git a/packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.tsx b/packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.tsx new file mode 100644 index 00000000..a29fcd80 --- /dev/null +++ b/packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.tsx @@ -0,0 +1,53 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { fetchDirectory } from '@supabase-cache-helpers/storage-core'; +import { fireEvent, screen } from '@testing-library/react'; + +import { useDirectory, useRemoveDirectory } from '../../src'; +import { cleanup, renderWithConfig, upload } from '../utils'; + +const TEST_PREFIX = 'postgrest-storage-remove'; + +describe('useRemoveDirectory', () => { + let client: SupabaseClient; + let dirName: string; + let files: string[]; + + beforeAll(async () => { + dirName = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + + await Promise.all([ + cleanup(client, 'public_contact_files', dirName), + cleanup(client, 'private_contact_files', dirName), + ]); + + files = await upload(client, 'private_contact_files', dirName); + }); + + it('should remove all files in a directory', async () => { + function Page() { + useDirectory(client.storage.from('private_contact_files'), dirName, { + refetchOnWindowFocus: false, + }); + const { mutateAsync: remove, isSuccess } = useRemoveDirectory( + client.storage.from('private_contact_files'), + ); + return ( + <> +

remove(dirName)} /> +
{`isSuccess: ${isSuccess}`}
+ + ); + } + + renderWithConfig(); + fireEvent.click(screen.getByTestId('remove')); + await screen.findByText('isSuccess: true', {}, { timeout: 10000 }); + await expect( + fetchDirectory(client.storage.from('private_contact_files'), dirName), + ).resolves.toEqual([]); + }); +}); diff --git a/packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.tsx b/packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.tsx new file mode 100644 index 00000000..a0582a86 --- /dev/null +++ b/packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.tsx @@ -0,0 +1,56 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { fetchDirectory } from '@supabase-cache-helpers/storage-core'; +import { fireEvent, screen } from '@testing-library/react'; + +import { useDirectory, useRemoveFiles } from '../../src'; +import { cleanup, renderWithConfig, upload } from '../utils'; + +const TEST_PREFIX = 'postgrest-storage-remove'; + +describe('useRemoveFiles', () => { + let client: SupabaseClient; + let dirName: string; + let files: string[]; + + beforeAll(async () => { + dirName = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + + await Promise.all([ + cleanup(client, 'public_contact_files', dirName), + cleanup(client, 'private_contact_files', dirName), + ]); + + files = await upload(client, 'private_contact_files', dirName); + }); + + it('should remove files', async () => { + function Page() { + useDirectory(client.storage.from('private_contact_files'), dirName, { + refetchOnWindowFocus: false, + }); + const { mutateAsync: remove, isSuccess } = useRemoveFiles( + client.storage.from('private_contact_files'), + ); + return ( + <> +
remove(files.map((f) => [dirName, f].join('/')))} + /> +
{`isSuccess: ${isSuccess}`}
+ + ); + } + + renderWithConfig(); + fireEvent.click(screen.getByTestId('remove')); + await screen.findByText('isSuccess: true', {}, { timeout: 10000 }); + await expect( + fetchDirectory(client.storage.from('private_contact_files'), dirName), + ).resolves.toEqual([]); + }); +}); diff --git a/packages/storage-vue-query/__tests__/mutate/use-upload.spec.tsx b/packages/storage-vue-query/__tests__/mutate/use-upload.spec.tsx new file mode 100644 index 00000000..7ff58f24 --- /dev/null +++ b/packages/storage-vue-query/__tests__/mutate/use-upload.spec.tsx @@ -0,0 +1,64 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { fetchDirectory } from '@supabase-cache-helpers/storage-core'; +import { fireEvent, screen } from '@testing-library/react'; + +import { useDirectory, useUpload } from '../../src'; +import { cleanup, loadFixtures, renderWithConfig } from '../utils'; + +const TEST_PREFIX = 'postgrest-storage-upload'; + +describe('useUpload', () => { + let client: SupabaseClient; + let dirName: string; + let fileNames: string[]; + let files: File[]; + + beforeAll(async () => { + dirName = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + + await Promise.all([ + cleanup(client, 'public_contact_files', dirName), + cleanup(client, 'private_contact_files', dirName), + ]); + + const fixtures = await loadFixtures(); + fileNames = fixtures.fileNames; + files = fixtures.files; + }); + + it('should upload files', async () => { + function Page() { + useDirectory(client.storage.from('private_contact_files'), dirName, { + refetchOnWindowFocus: false, + }); + const { mutateAsync: upload, isSuccess } = useUpload( + client.storage.from('private_contact_files'), + {}, + ); + return ( + <> +
upload({ files, path: dirName })} + /> +
{`isSuccess: ${isSuccess}`}
+ + ); + } + + renderWithConfig(); + fireEvent.click(screen.getByTestId('upload')); + await screen.findByText('isSuccess: true', {}, { timeout: 10000 }); + await expect( + fetchDirectory(client.storage.from('private_contact_files'), dirName), + ).resolves.toEqual( + expect.arrayContaining( + files.map((f) => expect.objectContaining({ name: f.name })), + ), + ); + }); +}); diff --git a/packages/storage-vue-query/__tests__/query/use-directory-urls.spec.tsx b/packages/storage-vue-query/__tests__/query/use-directory-urls.spec.tsx new file mode 100644 index 00000000..97b30acc --- /dev/null +++ b/packages/storage-vue-query/__tests__/query/use-directory-urls.spec.tsx @@ -0,0 +1,58 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { screen } from '@testing-library/react'; + +import { useDirectoryFileUrls } from '../../src'; +import { cleanup, renderWithConfig, upload } from '../utils'; + +const TEST_PREFIX = 'postgrest-storage-directory-urls'; + +describe('useDirectoryFileUrls', () => { + let client: SupabaseClient; + let dirName: string; + let privateFiles: string[]; + let publicFiles: string[]; + + beforeAll(async () => { + dirName = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + + await Promise.all([ + cleanup(client, 'public_contact_files', dirName), + cleanup(client, 'private_contact_files', dirName), + ]); + + privateFiles = await upload(client, 'private_contact_files', dirName); + publicFiles = await upload(client, 'public_contact_files', dirName); + }); + + it('should return files', async () => { + function Page() { + const { data: files } = useDirectoryFileUrls( + client.storage.from('private_contact_files'), + dirName, + 'private', + { + refetchOnWindowFocus: false, + }, + ); + return ( +
+ {(files ?? []).map((f) => ( + {`${f.name}: ${f.url ? 'exists' : f.url}`} + ))} +
+ ); + } + + renderWithConfig(); + await Promise.all( + privateFiles.map( + async (f) => + await screen.findByText(`${f}: exists`, {}, { timeout: 10000 }), + ), + ); + }); +}); diff --git a/packages/storage-vue-query/__tests__/query/use-directory.spec.tsx b/packages/storage-vue-query/__tests__/query/use-directory.spec.tsx new file mode 100644 index 00000000..224cde66 --- /dev/null +++ b/packages/storage-vue-query/__tests__/query/use-directory.spec.tsx @@ -0,0 +1,56 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { screen } from '@testing-library/react'; + +import { useDirectory } from '../../src'; +import { cleanup, renderWithConfig, upload } from '../utils'; + +const TEST_PREFIX = 'postgrest-storage-directory'; + +describe('useDirectory', () => { + let client: SupabaseClient; + let dirName: string; + let privateFiles: string[]; + let publicFiles: string[]; + + beforeAll(async () => { + dirName = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + + await Promise.all([ + cleanup(client, 'public_contact_files', dirName), + cleanup(client, 'private_contact_files', dirName), + ]); + + privateFiles = await upload(client, 'private_contact_files', dirName); + publicFiles = await upload(client, 'public_contact_files', dirName); + }); + + it('should return files', async () => { + function Page() { + const { data: files } = useDirectory( + client.storage.from('private_contact_files'), + dirName, + { + refetchOnWindowFocus: false, + }, + ); + return ( +
+ {(files ?? []).map((f) => ( + {f.name} + ))} +
+ ); + } + + renderWithConfig(); + await Promise.all( + privateFiles.map( + async (f) => await screen.findByText(f, {}, { timeout: 10000 }), + ), + ); + }); +}); diff --git a/packages/storage-vue-query/__tests__/query/use-file-url.spec.tsx b/packages/storage-vue-query/__tests__/query/use-file-url.spec.tsx new file mode 100644 index 00000000..71d83b2c --- /dev/null +++ b/packages/storage-vue-query/__tests__/query/use-file-url.spec.tsx @@ -0,0 +1,48 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { screen } from '@testing-library/react'; + +import { useFileUrl } from '../../src'; +import { cleanup, renderWithConfig, upload } from '../utils'; + +const TEST_PREFIX = 'postgrest-storage-file-url'; + +describe('useFileUrl', () => { + let client: SupabaseClient; + let dirName: string; + let privateFiles: string[]; + let publicFiles: string[]; + + beforeAll(async () => { + dirName = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + + await Promise.all([ + cleanup(client, 'public_contact_files', dirName), + cleanup(client, 'private_contact_files', dirName), + ]); + + privateFiles = await upload(client, 'private_contact_files', dirName); + publicFiles = await upload(client, 'public_contact_files', dirName); + }); + + it('should return file url', async () => { + function Page() { + const { data: url } = useFileUrl( + client.storage.from('public_contact_files'), + `${dirName}/${publicFiles[0]}`, + 'public', + { + ensureExistence: true, + refetchOnWindowFocus: false, + }, + ); + return
{`URL: ${url ? 'exists' : url}`}
; + } + + renderWithConfig(); + await screen.findByText('URL: exists', {}, { timeout: 10000 }); + }); +}); diff --git a/packages/storage-vue-query/__tests__/utils.tsx b/packages/storage-vue-query/__tests__/utils.tsx new file mode 100644 index 00000000..b0bda79d --- /dev/null +++ b/packages/storage-vue-query/__tests__/utils.tsx @@ -0,0 +1,65 @@ +import { SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render } from '@testing-library/react'; +import * as dotenv from 'dotenv'; +import { readdir, readFile } from 'node:fs/promises'; +import { resolve, join } from 'node:path'; +import React from 'react'; + +dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); + +export const renderWithConfig = ( + element: React.ReactElement, + queryClient?: QueryClient, +): ReturnType => { + const client = queryClient ?? new QueryClient(); + const TestQueryClientProvider = ({ + children, + }: { + children: React.ReactNode; + }) => {children} ; + return render(element, { wrapper: TestQueryClientProvider }); +}; + +export const loadFixtures = async () => { + const fixturesDir = resolve(__dirname, '__fixtures__'); + const fileNames = await readdir(fixturesDir); + return { + fileNames, + files: await Promise.all( + fileNames.map( + async (f) => + new File([(await readFile(join(fixturesDir, f))) as BlobPart], f), + ), + ), + }; +}; + +export const upload = async ( + client: SupabaseClient, + bucketName: string, + dirName: string, +): Promise => { + const fixturesDir = resolve(__dirname, '__fixtures__'); + const fileNames = await readdir(fixturesDir); + await Promise.all( + fileNames.map( + async (f) => + await client.storage + .from(bucketName) + .upload(`${dirName}/${f}`, await readFile(join(fixturesDir, f))), + ), + ); + return fileNames; +}; + +export const cleanup = async ( + client: SupabaseClient, + bucketName: string, + dirName: string, +) => { + const { data } = await client.storage.from(bucketName).list(dirName); + await client.storage + .from(bucketName) + .remove((data ?? []).map((d) => `${dirName}/${d.name}`)); +}; diff --git a/packages/storage-vue-query/package.json b/packages/storage-vue-query/package.json new file mode 100644 index 00000000..c2293525 --- /dev/null +++ b/packages/storage-vue-query/package.json @@ -0,0 +1,80 @@ +{ + "name": "@supabase-cache-helpers/storage-vue-query", + "version": "0.0.1", + "author": "Christian Pannwitz ", + "homepage": "https://supabase-cache-helpers.vercel.app", + "bugs": { + "url": "https://github.com/psteinroe/supabase-cache-helpers/issues" + }, + "main": "./dist/index.js", + "source": "./src/index.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "scripts": { + "build": "tsup", + "test": "jest --coverage", + "clean": "rm -rf .turbo && rm -rf lint-results && rm -rf .nyc_output && rm -rf node_modules && rm -rf dist", + "lint": "eslint {src/**,__tests__/**} --no-error-on-unmatched-pattern --ignore-pattern '__tests__/__fixtures__/*'", + "lint:report": "eslint {src/**,__tests__/**} --format json --output-file ./lint-results/storage-vue-query.json --no-error-on-unmatched-pattern --ignore-pattern '__tests__/__fixtures__/*'", + "lint:fix": "eslint {src/**,__tests__/**} --fix --no-error-on-unmatched-pattern --ignore-pattern '__tests__/__fixtures__/*'", + "typecheck": "tsc --pretty --noEmit", + "format:write": "prettier --write \"{src/**/*.{ts,tsx,md},__tests__/**/*.{ts,tsx,md}}\"", + "format:check": "prettier --check \"{src/**/*.{ts,tsx,md},__tests__/**/*.{ts,tsx,md}}\"" + }, + "keywords": [ + "Supabase", + "Storage", + "Cache", + "Tanstack Query", + "Vue Query" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/psteinroe/supabase-cache-helpers.git", + "directory": "packages/storage-vue-query" + }, + "peerDependencies": { + "@tanstack/vue-query": "^5.28.13", + "vue": "^3.4.21", + "@supabase/storage-js": "^2.4.0" + }, + "jest": { + "preset": "@supabase-cache-helpers/jest-presets/jest/node" + }, + "devDependencies": { + "@supabase/supabase-js": "2.38.5", + "@supabase/storage-js": "2.5.5", + "@testing-library/vue": "8.0.3", + "@testing-library/jest-dom": "6.4.0", + "jest-environment-jsdom": "29.7.0", + "@types/jest": "29.5.0", + "dotenv": "16.4.0", + "eslint": "8.54.0", + "@supabase-cache-helpers/eslint-config-custom": "workspace:*", + "jest": "29.7.0", + "@supabase-cache-helpers/jest-presets": "workspace:*", + "@supabase-cache-helpers/prettier-config": "workspace:*", + "ts-jest": "29.1.0", + "@supabase-cache-helpers/tsconfig": "workspace:*", + "tsup": "8.0.0", + "vue": "3.4.21", + "typescript": "5.4.2" + }, + "dependencies": { + "@supabase-cache-helpers/storage-core": "workspace:*" + } +} diff --git a/packages/storage-vue-query/prettier.config.cjs b/packages/storage-vue-query/prettier.config.cjs new file mode 100644 index 00000000..3fb75475 --- /dev/null +++ b/packages/storage-vue-query/prettier.config.cjs @@ -0,0 +1 @@ +module.exports = require("@supabase-cache-helpers/prettier-config/prettier.config.js"); diff --git a/packages/storage-vue-query/src/index.ts b/packages/storage-vue-query/src/index.ts new file mode 100644 index 00000000..55e3ffb8 --- /dev/null +++ b/packages/storage-vue-query/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib'; +export * from './mutate'; +export * from './query'; diff --git a/packages/storage-vue-query/src/lib/constants.ts b/packages/storage-vue-query/src/lib/constants.ts new file mode 100644 index 00000000..1bcb3320 --- /dev/null +++ b/packages/storage-vue-query/src/lib/constants.ts @@ -0,0 +1,2 @@ +export const KEY_PREFIX = 'storage'; +export const KEY_SEPARATOR = '$'; diff --git a/packages/storage-vue-query/src/lib/decode.ts b/packages/storage-vue-query/src/lib/decode.ts new file mode 100644 index 00000000..3ad74e9e --- /dev/null +++ b/packages/storage-vue-query/src/lib/decode.ts @@ -0,0 +1,12 @@ +import { DecodedStorageKey } from '@supabase-cache-helpers/storage-core'; +import { QueryKey } from '@tanstack/vue-query'; + +import { KEY_PREFIX } from './constants'; + +export const decode = (key: QueryKey): DecodedStorageKey | null => { + if (!Array.isArray(key) || key.length !== 3 || key[0] !== KEY_PREFIX) { + return null; + } + const [_, bucketId, path] = key; + return { bucketId, path }; +}; diff --git a/packages/storage-vue-query/src/lib/encode.ts b/packages/storage-vue-query/src/lib/encode.ts new file mode 100644 index 00000000..c13f3712 --- /dev/null +++ b/packages/storage-vue-query/src/lib/encode.ts @@ -0,0 +1,10 @@ +import { QueryKey } from '@tanstack/vue-query'; + +import { KEY_PREFIX } from './constants'; +import { getBucketId } from './get-bucket-id'; +import { assertStorageKeyInput } from './key'; + +export const encode = (key: QueryKey): string[] => { + const [fileApi, path] = assertStorageKeyInput(key); + return [KEY_PREFIX, getBucketId(fileApi), path]; +}; diff --git a/packages/storage-vue-query/src/lib/get-bucket-id.ts b/packages/storage-vue-query/src/lib/get-bucket-id.ts new file mode 100644 index 00000000..ccc1157a --- /dev/null +++ b/packages/storage-vue-query/src/lib/get-bucket-id.ts @@ -0,0 +1,4 @@ +import { StorageFileApi } from './types'; + +export const getBucketId = (fileApi: StorageFileApi) => + fileApi['bucketId'] as string; diff --git a/packages/storage-vue-query/src/lib/index.ts b/packages/storage-vue-query/src/lib/index.ts new file mode 100644 index 00000000..510a94af --- /dev/null +++ b/packages/storage-vue-query/src/lib/index.ts @@ -0,0 +1,7 @@ +export * from './constants'; +export * from './decode'; +export * from './encode'; +export * from './get-bucket-id'; +export * from './key'; +export * from './truthy'; +export * from './types'; diff --git a/packages/storage-vue-query/src/lib/key.ts b/packages/storage-vue-query/src/lib/key.ts new file mode 100644 index 00000000..6640dce9 --- /dev/null +++ b/packages/storage-vue-query/src/lib/key.ts @@ -0,0 +1,16 @@ +import { QueryKey } from '@tanstack/vue-query'; + +import { StorageFileApi } from './types'; + +export const isStorageKeyInput = (key: QueryKey): key is StorageKeyInput => + Array.isArray(key) && + key.length === 2 && + typeof key[1] === 'string' && + Boolean(key[0]['bucketId']); + +export const assertStorageKeyInput = (key: QueryKey): StorageKeyInput => { + if (!isStorageKeyInput(key)) throw new Error('Invalid key'); + return key; +}; + +export type StorageKeyInput = [StorageFileApi, string]; diff --git a/packages/storage-vue-query/src/lib/truthy.ts b/packages/storage-vue-query/src/lib/truthy.ts new file mode 100644 index 00000000..c2eabd7e --- /dev/null +++ b/packages/storage-vue-query/src/lib/truthy.ts @@ -0,0 +1,5 @@ +type Truthy = T extends false | '' | 0 | null | undefined ? never : T; // from lodash + +export function truthy(value: T): value is Truthy { + return !!value; +} diff --git a/packages/storage-vue-query/src/lib/types.ts b/packages/storage-vue-query/src/lib/types.ts new file mode 100644 index 00000000..fc88d47a --- /dev/null +++ b/packages/storage-vue-query/src/lib/types.ts @@ -0,0 +1,3 @@ +import { SupabaseClient } from '@supabase/supabase-js'; + +export type StorageFileApi = ReturnType; diff --git a/packages/storage-vue-query/src/mutate/index.ts b/packages/storage-vue-query/src/mutate/index.ts new file mode 100644 index 00000000..37f5c85a --- /dev/null +++ b/packages/storage-vue-query/src/mutate/index.ts @@ -0,0 +1,3 @@ +export * from './use-remove-directory'; +export * from './use-remove-files'; +export * from './use-upload'; diff --git a/packages/storage-vue-query/src/mutate/use-remove-directory.ts b/packages/storage-vue-query/src/mutate/use-remove-directory.ts new file mode 100644 index 00000000..74ce8a02 --- /dev/null +++ b/packages/storage-vue-query/src/mutate/use-remove-directory.ts @@ -0,0 +1,51 @@ +import { FileObject, StorageError } from '@supabase/storage-js'; +import { + createRemoveDirectoryFetcher, + mutatePaths, +} from '@supabase-cache-helpers/storage-core'; +import { + QueryKey, + useMutation, + UseMutationOptions, + UseMutationResult, + useQueryClient, +} from '@tanstack/react-query'; +import { useCallback } from 'react'; + +import { decode, getBucketId, StorageFileApi } from '../lib'; + +/** + * A hook that provides a mutation function to remove a directory and all its contents. + * @param fileApi The `StorageFileApi` instance to use for the removal. + * @param config Optional configuration options for the React Query mutation. + * @returns An object containing the mutation function, loading state, and error state. + */ +function useRemoveDirectory( + fileApi: StorageFileApi, + config?: Omit< + UseMutationOptions, + 'mutationFn' + >, +): UseMutationResult { + const queryClient = useQueryClient(); + const fetcher = useCallback(createRemoveDirectoryFetcher(fileApi), [fileApi]); + return useMutation({ + mutationFn: async (arg) => { + const result = fetcher(arg); + await mutatePaths(getBucketId(fileApi), [arg], { + cacheKeys: queryClient + .getQueryCache() + .getAll() + .map((c) => c.queryKey), + decode, + mutate: async (key) => { + await queryClient.invalidateQueries({ queryKey: key }); + }, + }); + return result; + }, + ...config, + }); +} + +export { useRemoveDirectory }; diff --git a/packages/storage-vue-query/src/mutate/use-remove-files.ts b/packages/storage-vue-query/src/mutate/use-remove-files.ts new file mode 100644 index 00000000..fb0820cd --- /dev/null +++ b/packages/storage-vue-query/src/mutate/use-remove-files.ts @@ -0,0 +1,51 @@ +import { FileObject, StorageError } from '@supabase/storage-js'; +import { + createRemoveFilesFetcher, + mutatePaths, +} from '@supabase-cache-helpers/storage-core'; +import { + QueryKey, + useMutation, + UseMutationOptions, + UseMutationResult, + useQueryClient, +} from '@tanstack/react-query'; +import { useCallback } from 'react'; + +import { decode, getBucketId, StorageFileApi } from '../lib'; + +/** + * Hook for removing files from storage using React Query mutation + * @param {StorageFileApi} fileApi - The Supabase Storage API + * @param {UseMutationOptions} [config] - The React Query mutation configuration + * @returns {UseMutationOptions} - The React Query mutation response object + */ +function useRemoveFiles( + fileApi: StorageFileApi, + config?: Omit< + UseMutationOptions, + 'mutationFn' + >, +): UseMutationResult { + const queryClient = useQueryClient(); + const fetcher = useCallback(createRemoveFilesFetcher(fileApi), [fileApi]); + return useMutation({ + mutationFn: async (paths) => { + const res = await fetcher(paths); + await mutatePaths(getBucketId(fileApi), paths, { + cacheKeys: queryClient + .getQueryCache() + .getAll() + .map((c) => c.queryKey), + decode, + mutate: async (key) => { + await queryClient.invalidateQueries({ queryKey: key }); + }, + }); + return res; + }, + ...config, + }); +} + +export { useRemoveFiles }; diff --git a/packages/storage-vue-query/src/mutate/use-upload.ts b/packages/storage-vue-query/src/mutate/use-upload.ts new file mode 100644 index 00000000..40c77eae --- /dev/null +++ b/packages/storage-vue-query/src/mutate/use-upload.ts @@ -0,0 +1,70 @@ +import { FileObject, StorageError } from '@supabase/storage-js'; +import { + FileInput, + createUploadFetcher, + UploadFetcherConfig, + UploadFileResponse, + mutatePaths, +} from '@supabase-cache-helpers/storage-core'; +import { + useMutation, + UseMutationOptions, + UseMutationReturnType, + useQueryClient, +} from '@tanstack/vue-query'; + +import { decode, getBucketId, StorageFileApi, truthy } from '../lib'; + +export type { UploadFetcherConfig, UploadFileResponse, FileInput }; + +export type UseUploadInput = { + file: FileObject; + files: FileList | (File | FileInput)[]; + path?: string; +}; + +/** + * Hook for uploading files to storage using React Query mutation + * @param {StorageFileApi} fileApi - The Supabase Storage API + * @param {UploadFetcherConfig & UseMutationOptions} [config] - The React Query mutation configuration + * @returns {UseMutationResult} - The React Query mutation response object + */ +function useUpload( + fileApi: StorageFileApi, + config?: UploadFetcherConfig & + Omit< + UseMutationOptions, + 'mutationFn' + >, +): UseMutationReturnType< + UploadFileResponse[], + StorageError, + UseUploadInput, + unknown +> { + const queryClient = useQueryClient(); + const fetcher = createUploadFetcher(fileApi, config); + return useMutation({ + mutationFn: async ({ files, path }) => { + const result = await fetcher(files, path); + await mutatePaths( + getBucketId(fileApi), + result.map(({ data }) => data?.path).filter(truthy), + { + cacheKeys: queryClient + .getQueryCache() + .getAll() + .map((c) => c.queryKey), + decode, + mutate: async (key) => { + await queryClient.invalidateQueries({ queryKey: key }); + }, + }, + ); + return result; + }, + ...config, + }); +} + +export { useUpload }; diff --git a/packages/storage-vue-query/src/query/index.ts b/packages/storage-vue-query/src/query/index.ts new file mode 100644 index 00000000..4717db82 --- /dev/null +++ b/packages/storage-vue-query/src/query/index.ts @@ -0,0 +1,3 @@ +export * from './use-directory-urls'; +export * from './use-directory'; +export * from './use-file-url'; diff --git a/packages/storage-vue-query/src/query/use-directory-urls.ts b/packages/storage-vue-query/src/query/use-directory-urls.ts new file mode 100644 index 00000000..690b9df5 --- /dev/null +++ b/packages/storage-vue-query/src/query/use-directory-urls.ts @@ -0,0 +1,70 @@ +import { FileObject, StorageError } from '@supabase/storage-js'; +import { + createDirectoryUrlsFetcher, + StoragePrivacy, + URLFetcherConfig, +} from '@supabase-cache-helpers/storage-core'; +import { + useQuery as useVueQuery, + UseQueryReturnType as UseVueQueryResult, + UseQueryOptions as UseVueQueryOptions, +} from '@tanstack/vue-query'; + +import { encode, StorageFileApi } from '../lib'; + +function buildDirectoryUrlsQueryOpts( + fileApi: StorageFileApi, + path: string, + mode: StoragePrivacy, + config?: Omit< + UseVueQueryOptions< + (FileObject & { url: string })[] | undefined, + StorageError + >, + 'queryKey' | 'queryFn' + > & + Pick, +): UseVueQueryOptions< + (FileObject & { url: string })[] | undefined, + StorageError +> { + return { + queryKey: encode([fileApi, path]), + queryFn: () => createDirectoryUrlsFetcher(mode, config)(fileApi, path), + ...config, + }; +} + +/** + * Convenience hook to fetch all files in a directory, and their corresponding URLs, from Supabase Storage using Vue Query. + * + * @param {StorageFileApi} fileApi - The file API of the storage bucket. + * @param {string|null} path - The path of the directory to fetch files from. + * @param {StoragePrivacy} mode - The privacy mode of the bucket to fetch files from. + * @param {UseQueryOptions & Pick} [config] - Optional SWR configuration and `expiresIn` value to pass to the `createDirectoryUrlsFetcher` function. + * + * @returns {UseQueryResult<(FileObject & { url: string })[] | undefined, StorageError>} A Vue Query response containing an array of file objects with their corresponding URLs. + */ +function useDirectoryFileUrls( + fileApi: StorageFileApi, + path: string, + mode: StoragePrivacy, + config?: Omit< + UseVueQueryOptions< + (FileObject & { url: string })[] | undefined, + StorageError + >, + 'queryKey' | 'queryFn' + > & + Pick, +): UseVueQueryResult< + (FileObject & { url: string })[] | undefined, + StorageError +> { + return useVueQuery< + (FileObject & { url: string })[] | undefined, + StorageError + >(buildDirectoryUrlsQueryOpts(fileApi, path, mode, config)); +} + +export { useDirectoryFileUrls, buildDirectoryUrlsQueryOpts }; diff --git a/packages/storage-vue-query/src/query/use-directory.ts b/packages/storage-vue-query/src/query/use-directory.ts new file mode 100644 index 00000000..1285bb83 --- /dev/null +++ b/packages/storage-vue-query/src/query/use-directory.ts @@ -0,0 +1,45 @@ +import { FileObject, StorageError } from '@supabase/storage-js'; +import { fetchDirectory } from '@supabase-cache-helpers/storage-core'; +import { + useQuery as useVueQuery, + UseQueryReturnType as UseVueQueryResult, + UseQueryOptions as UseVueQueryOptions, +} from '@tanstack/vue-query'; + +import { StorageFileApi, encode } from '../lib'; + +function buildDirectoryQueryOpts( + fileApi: StorageFileApi, + path: string, + config?: Omit< + UseVueQueryOptions, + 'queryKey' | 'queryFn' + >, +): UseVueQueryOptions { + return { + queryKey: encode([fileApi, path]), + queryFn: () => fetchDirectory(fileApi, path), + ...config, + }; +} + +/** + * Convenience hook to fetch a directory from Supabase Storage using Vue Query. + * + * @param fileApi The StorageFileApi instance. + * @param path The path to the directory. + * @param config The Vue Query configuration. + * @returns An UseQueryResult containing an array of FileObjects + */ +function useDirectory( + fileApi: StorageFileApi, + path: string, + config?: Omit< + UseVueQueryOptions, + 'queryKey' | 'queryFn' + >, +): UseVueQueryResult { + return useVueQuery(buildDirectoryQueryOpts(fileApi, path, config)); +} + +export { useDirectory }; diff --git a/packages/storage-vue-query/src/query/use-file-url.ts b/packages/storage-vue-query/src/query/use-file-url.ts new file mode 100644 index 00000000..7bcb2b26 --- /dev/null +++ b/packages/storage-vue-query/src/query/use-file-url.ts @@ -0,0 +1,56 @@ +import { StorageError } from '@supabase/storage-js'; +import { + StoragePrivacy, + createUrlFetcher, + URLFetcherConfig, +} from '@supabase-cache-helpers/storage-core'; +import { + useQuery as useVueQuery, + UseQueryReturnType as UseVueQueryResult, + UseQueryOptions as UseVueQueryOptions, +} from '@tanstack/vue-query'; + +import { StorageFileApi, encode } from '../lib'; + +function buildFileUrlQueryOpts( + fileApi: StorageFileApi, + path: string, + mode: StoragePrivacy, + config?: Omit< + UseVueQueryOptions, + 'queryKey' | 'queryFn' + > & + URLFetcherConfig, +): UseVueQueryOptions { + return { + queryKey: encode([fileApi, path]), + queryFn: () => createUrlFetcher(mode, config)(fileApi, path), + ...config, + }; +} + +/** + * A hook to fetch the URL for a file in the Storage. + * + * @param fileApi - the file API instance from the Supabase client. + * @param path - the path of the file to fetch the URL for. + * @param mode - the privacy mode of the bucket the file is in. + * @param config - the Vue Query configuration options and URL fetcher configuration. + * @returns the Vue Query response for the URL of the file + */ +function useFileUrl( + fileApi: StorageFileApi, + path: string, + mode: StoragePrivacy, + config?: Omit< + UseVueQueryOptions, + 'queryKey' | 'queryFn' + > & + URLFetcherConfig, +): UseVueQueryResult { + return useVueQuery( + buildFileUrlQueryOpts(fileApi, path, mode, config), + ); +} + +export { useFileUrl }; diff --git a/packages/storage-vue-query/tsconfig.json b/packages/storage-vue-query/tsconfig.json new file mode 100644 index 00000000..c5f94dfa --- /dev/null +++ b/packages/storage-vue-query/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@supabase-cache-helpers/tsconfig/web.json", + "include": ["**/*.ts"], + "exclude": ["node_modules"], + "compilerOptions": { + "noErrorTruncation": true + } +} diff --git a/packages/storage-vue-query/tsup.config.ts b/packages/storage-vue-query/tsup.config.ts new file mode 100644 index 00000000..7cfe8a38 --- /dev/null +++ b/packages/storage-vue-query/tsup.config.ts @@ -0,0 +1,15 @@ +import type { Options } from 'tsup'; + +export const tsup: Options = { + dts: true, + entryPoints: ['src/index.ts'], + external: ['vue', /^@supabase\//], + format: ['cjs', 'esm'], + // inject: ['src/react-shim.js'], + // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default! + legacyOutput: false, + sourcemap: true, + splitting: false, + bundle: true, + clean: true, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21731fed..b190daa8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -964,6 +964,67 @@ importers: specifier: 5.4.2 version: 5.4.2 + packages/storage-vue-query: + dependencies: + '@supabase-cache-helpers/storage-core': + specifier: workspace:* + version: link:../storage-core + '@tanstack/vue-query': + specifier: ^5.28.13 + version: 5.28.13(vue@3.4.21) + devDependencies: + '@supabase-cache-helpers/eslint-config-custom': + specifier: workspace:* + version: link:../eslint-config-custom + '@supabase-cache-helpers/jest-presets': + specifier: workspace:* + version: link:../jest-presets + '@supabase-cache-helpers/prettier-config': + specifier: workspace:* + version: link:../prettier-config + '@supabase-cache-helpers/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@supabase/storage-js': + specifier: 2.5.5 + version: 2.5.5 + '@supabase/supabase-js': + specifier: 2.38.5 + version: 2.38.5 + '@testing-library/jest-dom': + specifier: 6.4.0 + version: 6.4.0(@types/jest@29.5.0)(jest@29.7.0) + '@testing-library/vue': + specifier: 8.0.3 + version: 8.0.3(vue@3.4.21) + '@types/jest': + specifier: 29.5.0 + version: 29.5.0 + dotenv: + specifier: 16.4.0 + version: 16.4.0 + eslint: + specifier: 8.54.0 + version: 8.54.0 + jest: + specifier: 29.7.0 + version: 29.7.0 + jest-environment-jsdom: + specifier: 29.7.0 + version: 29.7.0 + ts-jest: + specifier: 29.1.0 + version: 29.1.0(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2) + tsup: + specifier: 8.0.0 + version: 8.0.0(typescript@5.4.2) + typescript: + specifier: 5.4.2 + version: 5.4.2 + vue: + specifier: 3.4.21 + version: 3.4.21(typescript@5.4.2) + packages/tsconfig: {} packages: @@ -2265,8 +2326,8 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.20 - '@types/node': 17.0.45 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 20.10.1 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -2323,9 +2384,9 @@ packages: resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -4440,7 +4501,7 @@ packages: /@types/babel__core@7.1.19: resolution: {integrity: sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==} dependencies: - '@babel/parser': 7.23.0 + '@babel/parser': 7.24.0 '@babel/types': 7.23.0 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 @@ -4454,7 +4515,7 @@ packages: /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: - '@babel/parser': 7.23.0 + '@babel/parser': 7.24.0 '@babel/types': 7.23.0 /@types/babel__traverse@7.18.0: @@ -4904,7 +4965,7 @@ packages: '@vue/shared': 3.4.21 entities: 4.5.0 estree-walker: 2.0.2 - source-map-js: 1.0.2 + source-map-js: 1.2.0 /@vue/compiler-dom@3.4.21: resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} @@ -4923,7 +4984,7 @@ packages: estree-walker: 2.0.2 magic-string: 0.30.9 postcss: 8.4.38 - source-map-js: 1.0.2 + source-map-js: 1.2.0 /@vue/compiler-ssr@3.4.21: resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==} @@ -5282,7 +5343,7 @@ packages: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} dependencies: - '@babel/helper-plugin-utils': 7.18.9 + '@babel/helper-plugin-utils': 7.22.5 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.0 @@ -8311,8 +8372,8 @@ packages: resolution: {integrity: sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==} engines: {node: '>=10'} dependencies: - '@babel/core': 7.23.2 - '@babel/parser': 7.23.0 + '@babel/core': 7.24.0 + '@babel/parser': 7.24.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 7.5.4 @@ -8847,7 +8908,7 @@ packages: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.14.2 + ws: 8.16.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -12806,19 +12867,6 @@ packages: signal-exit: 4.1.0 dev: true - /ws@8.14.2: - resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - /ws@8.16.0: resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} engines: {node: '>=10.0.0'} From ce5b96706bcfd1068ecb201a8f15b2a5103d18ad Mon Sep 17 00:00:00 2001 From: Christian Pannwitz Date: Thu, 18 Apr 2024 13:42:46 +0200 Subject: [PATCH 4/5] feat(postgrest-vue-query): finalized tests --- packages/postgrest-vue-query/.eslintrc.json | 10 +- ...-item.spec.tsx => use-mutate-item.spec.ts} | 45 +--- .../components/DeleteManyMutationPage.vue | 77 +++++++ .../components/DeleteMutationPage-1.vue | 59 +++++ .../components/DeleteMutationPage-2.vue | 67 ++++++ .../components/InsertMutationPage.vue | 49 ++++ .../__tests__/components/MutateItemPage.vue | 40 ++++ .../__tests__/components/QueryPage-1.vue | 20 ++ .../__tests__/components/QueryPage-2.vue | 20 ++ .../__tests__/components/QueryPage-3.vue | 26 +++ .../__tests__/components/QueryPage-4.vue | 30 +++ .../__tests__/components/QueryPage-5.vue | 33 +++ .../__tests__/components/QueryPage-6.vue | 29 +++ .../__tests__/components/QueryPage-7.vue | 36 +++ .../__tests__/components/SubscriptionPage.vue | 51 ++++ .../components/SubscriptionQueryPage.vue | 55 +++++ .../components/UpdateMutationPage.vue | 59 +++++ .../components/UpsertMutationPage.vue | 57 +++++ ...e-delete-many-mutation.integration.spec.ts | 68 ++++++ ...-delete-many-mutation.integration.spec.tsx | 133 ----------- .../use-delete-mutation.integration.spec.ts | 113 +++++++++ .../use-delete-mutation.integration.spec.tsx | 217 ------------------ .../use-insert-mutation.integration.spec.ts | 47 ++++ .../use-insert-mutation.integration.spec.tsx | 79 ------- .../use-update-mutation.integration.spec.ts | 43 ++++ .../use-update-mutation.integration.spec.tsx | 87 ------- .../use-upsert-mutation.integration.spec.ts | 48 ++++ .../use-upsert-mutation.integration.spec.tsx | 92 -------- .../__tests__/query/fetch.spec.ts | 4 +- .../query/prefetch.integration.spec.ts | 4 +- ...spec.tsx => use-query.integration.spec.ts} | 148 +++--------- ...se-subscription-query-integration.spec.tsx | 112 --------- ...use-subscription-query.integration.spec.ts | 70 ++++++ .../use-subscription.integration.spec.ts | 61 +++++ .../use-subscription.integration.spec.tsx | 92 -------- .../postgrest-vue-query/__tests__/utils.ts | 20 ++ .../postgrest-vue-query/__tests__/utils.tsx | 21 -- packages/postgrest-vue-query/package.json | 25 +- .../postgrest-vue-query/src/mutate/types.ts | 2 +- .../src/mutate/use-delete-many-mutation.ts | 7 +- .../src/mutate/use-delete-mutation.ts | 7 +- .../src/mutate/use-insert-mutation.ts | 7 +- .../src/mutate/use-update-mutation.ts | 7 +- .../src/mutate/use-upsert-mutation.ts | 7 +- .../src/query/use-query.ts | 22 +- packages/storage-vue-query/.eslintrc.json | 10 +- .../__tests__/components/DirectoryPage.vue | 24 ++ .../components/DirectoryUrlsPage.vue | 24 ++ .../__tests__/components/FileUrlPage.vue | 24 ++ .../components/RemoveDirectoryPage.vue | 27 +++ .../__tests__/components/RemoveFilesPage.vue | 31 +++ .../__tests__/components/UploadPage.vue | 29 +++ ....spec.tsx => use-remove-directory.spec.ts} | 22 +- ...iles.spec.tsx => use-remove-files.spec.ts} | 23 +- ...use-upload.spec.tsx => use-upload.spec.ts} | 24 +- ...ls.spec.tsx => use-directory-urls.spec.ts} | 23 +- ...rectory.spec.tsx => use-directory.spec.ts} | 22 +- ...file-url.spec.tsx => use-file-url.spec.ts} | 18 +- .../__tests__/{utils.tsx => utils.ts} | 20 +- packages/storage-vue-query/package.json | 23 +- .../src/mutate/use-upload.ts | 1 - pnpm-lock.yaml | 169 +++++++++++--- 62 files changed, 1616 insertions(+), 1204 deletions(-) rename packages/postgrest-vue-query/__tests__/cache/{use-mutate-item.spec.tsx => use-mutate-item.spec.ts} (53%) create mode 100644 packages/postgrest-vue-query/__tests__/components/DeleteManyMutationPage.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/DeleteMutationPage-1.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/DeleteMutationPage-2.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/InsertMutationPage.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/MutateItemPage.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/QueryPage-1.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/QueryPage-2.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/QueryPage-3.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/QueryPage-4.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/QueryPage-5.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/QueryPage-6.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/QueryPage-7.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/SubscriptionPage.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/SubscriptionQueryPage.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/UpdateMutationPage.vue create mode 100644 packages/postgrest-vue-query/__tests__/components/UpsertMutationPage.vue create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.ts delete mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.ts delete mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.ts delete mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.ts delete mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.ts delete mode 100644 packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.tsx rename packages/postgrest-vue-query/__tests__/query/{use-query.integration.spec.tsx => use-query.integration.spec.ts} (54%) delete mode 100644 packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query-integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query.integration.spec.ts create mode 100644 packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.ts delete mode 100644 packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.tsx create mode 100644 packages/postgrest-vue-query/__tests__/utils.ts delete mode 100644 packages/postgrest-vue-query/__tests__/utils.tsx create mode 100644 packages/storage-vue-query/__tests__/components/DirectoryPage.vue create mode 100644 packages/storage-vue-query/__tests__/components/DirectoryUrlsPage.vue create mode 100644 packages/storage-vue-query/__tests__/components/FileUrlPage.vue create mode 100644 packages/storage-vue-query/__tests__/components/RemoveDirectoryPage.vue create mode 100644 packages/storage-vue-query/__tests__/components/RemoveFilesPage.vue create mode 100644 packages/storage-vue-query/__tests__/components/UploadPage.vue rename packages/storage-vue-query/__tests__/mutate/{use-remove-directory.spec.tsx => use-remove-directory.spec.ts} (65%) rename packages/storage-vue-query/__tests__/mutate/{use-remove-files.spec.tsx => use-remove-files.spec.ts} (65%) rename packages/storage-vue-query/__tests__/mutate/{use-upload.spec.tsx => use-upload.spec.ts} (69%) rename packages/storage-vue-query/__tests__/query/{use-directory-urls.spec.tsx => use-directory-urls.spec.ts} (68%) rename packages/storage-vue-query/__tests__/query/{use-directory.spec.tsx => use-directory.spec.ts} (71%) rename packages/storage-vue-query/__tests__/query/{use-file-url.spec.tsx => use-file-url.spec.ts} (71%) rename packages/storage-vue-query/__tests__/{utils.tsx => utils.ts} (78%) diff --git a/packages/postgrest-vue-query/.eslintrc.json b/packages/postgrest-vue-query/.eslintrc.json index c54a0612..de95d41d 100644 --- a/packages/postgrest-vue-query/.eslintrc.json +++ b/packages/postgrest-vue-query/.eslintrc.json @@ -1,4 +1,12 @@ { "root": true, - "extends": ["@supabase-cache-helpers/custom"] + "parser": "vue-eslint-parser", + "parserOptions": { + "parser": "@typescript-eslint/parser" + }, + "extends": [ + "@supabase-cache-helpers/custom", + "plugin:vue/vue3-recommended", + "@vue/typescript/recommended" + ] } diff --git a/packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.tsx b/packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.ts similarity index 53% rename from packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.tsx rename to packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.ts index d40d49d1..6aa547a9 100644 --- a/packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.tsx +++ b/packages/postgrest-vue-query/__tests__/cache/use-mutate-item.spec.ts @@ -1,13 +1,12 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; -import { fireEvent, screen } from '@testing-library/react'; -import React from 'react'; +import { QueryClient } from '@tanstack/vue-query'; +import { fireEvent, screen } from '@testing-library/vue'; -import { useMutateItem, useQuery } from '../../src'; import type { Database } from '../database.types'; import { renderWithConfig } from '../utils'; +import Page from '../components/MutateItemPage.vue'; -const TEST_PREFIX = 'postgrest-react-query-mutate-item'; +const TEST_PREFIX = 'postgrest-vue-query-mutate-item'; describe('useMutateItem', () => { let client: SupabaseClient; @@ -39,42 +38,8 @@ describe('useMutateItem', () => { it('should mutate existing item in cache', async () => { const queryClient = new QueryClient(); - function Page() { - const { data, count } = useQuery( - client - .from('contact') - .select('id,username', { count: 'exact' }) - .ilike('username', `${testRunPrefix}%`), - ); - const mutate = useMutateItem({ - schema: 'public', - table: 'contact', - primaryKeys: ['id'], - }); - - return ( -
-
- await mutate( - { - id: (data ?? []).find((c) => c)?.id, - }, - (c) => ({ ...c, username: `${c.username}-updated` }), - ) - } - /> - {(data ?? []).map((d) => ( - {d.username} - ))} - {`count: ${count}`} -
- ); - } - - renderWithConfig(, queryClient); + renderWithConfig(Page, { client, testRunPrefix }, queryClient); await screen.findByText( `count: ${contacts.length}`, {}, diff --git a/packages/postgrest-vue-query/__tests__/components/DeleteManyMutationPage.vue b/packages/postgrest-vue-query/__tests__/components/DeleteManyMutationPage.vue new file mode 100644 index 00000000..a6003f40 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/DeleteManyMutationPage.vue @@ -0,0 +1,77 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/DeleteMutationPage-1.vue b/packages/postgrest-vue-query/__tests__/components/DeleteMutationPage-1.vue new file mode 100644 index 00000000..a3a1969d --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/DeleteMutationPage-1.vue @@ -0,0 +1,59 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/DeleteMutationPage-2.vue b/packages/postgrest-vue-query/__tests__/components/DeleteMutationPage-2.vue new file mode 100644 index 00000000..342a7b9f --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/DeleteMutationPage-2.vue @@ -0,0 +1,67 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/InsertMutationPage.vue b/packages/postgrest-vue-query/__tests__/components/InsertMutationPage.vue new file mode 100644 index 00000000..89758df6 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/InsertMutationPage.vue @@ -0,0 +1,49 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/MutateItemPage.vue b/packages/postgrest-vue-query/__tests__/components/MutateItemPage.vue new file mode 100644 index 00000000..c74db30c --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/MutateItemPage.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/QueryPage-1.vue b/packages/postgrest-vue-query/__tests__/components/QueryPage-1.vue new file mode 100644 index 00000000..c4ef23da --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/QueryPage-1.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/QueryPage-2.vue b/packages/postgrest-vue-query/__tests__/components/QueryPage-2.vue new file mode 100644 index 00000000..15a24e43 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/QueryPage-2.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/QueryPage-3.vue b/packages/postgrest-vue-query/__tests__/components/QueryPage-3.vue new file mode 100644 index 00000000..698e7e2c --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/QueryPage-3.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/QueryPage-4.vue b/packages/postgrest-vue-query/__tests__/components/QueryPage-4.vue new file mode 100644 index 00000000..751de292 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/QueryPage-4.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/QueryPage-5.vue b/packages/postgrest-vue-query/__tests__/components/QueryPage-5.vue new file mode 100644 index 00000000..d092b2c5 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/QueryPage-5.vue @@ -0,0 +1,33 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/QueryPage-6.vue b/packages/postgrest-vue-query/__tests__/components/QueryPage-6.vue new file mode 100644 index 00000000..5ab0a6e4 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/QueryPage-6.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/QueryPage-7.vue b/packages/postgrest-vue-query/__tests__/components/QueryPage-7.vue new file mode 100644 index 00000000..b81a357d --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/QueryPage-7.vue @@ -0,0 +1,36 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/SubscriptionPage.vue b/packages/postgrest-vue-query/__tests__/components/SubscriptionPage.vue new file mode 100644 index 00000000..cfa7bf19 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/SubscriptionPage.vue @@ -0,0 +1,51 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/SubscriptionQueryPage.vue b/packages/postgrest-vue-query/__tests__/components/SubscriptionQueryPage.vue new file mode 100644 index 00000000..2c5cfc92 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/SubscriptionQueryPage.vue @@ -0,0 +1,55 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/UpdateMutationPage.vue b/packages/postgrest-vue-query/__tests__/components/UpdateMutationPage.vue new file mode 100644 index 00000000..88843d63 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/UpdateMutationPage.vue @@ -0,0 +1,59 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/components/UpsertMutationPage.vue b/packages/postgrest-vue-query/__tests__/components/UpsertMutationPage.vue new file mode 100644 index 00000000..05499e83 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/components/UpsertMutationPage.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.ts b/packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.ts new file mode 100644 index 00000000..03b6b1ac --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.ts @@ -0,0 +1,68 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/vue-query'; +import { fireEvent, screen } from '@testing-library/vue'; + +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; +import Page from '../components/DeleteManyMutationPage.vue'; + +const TEST_PREFIX = 'postgrest-vue-query-delmany'; + +describe('useDeleteManyMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + let contacts: Database['public']['Tables']['contact']['Row'][]; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + }); + + beforeEach(async () => { + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + + const { data } = await client + .from('contact') + .insert( + new Array(3) + .fill(0) + .map((idx) => ({ username: `${testRunPrefix}-${idx}` })), + ) + .select('*'); + contacts = data as Database['public']['Tables']['contact']['Row'][]; + }); + + it('should delete existing cache item and reduce count', async () => { + const queryClient = new QueryClient(); + + renderWithConfig(Page, { client, contacts }, queryClient); + await screen.findByText( + `count: ${contacts.length}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('deleteWithEmptyOptions')); + await screen.findByText( + `count: ${contacts.length - 1}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('deleteWithoutOptions')); + await screen.findByText( + `count: ${contacts.length - 2}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('delete')); + await screen.findByText('success: true', {}, { timeout: 10000 }); + await screen.findByText( + `count: ${contacts.length - 3}`, + {}, + { timeout: 10000 }, + ); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.tsx deleted file mode 100644 index e6f5d851..00000000 --- a/packages/postgrest-vue-query/__tests__/mutate/use-delete-many-mutation.integration.spec.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; -import { fireEvent, screen } from '@testing-library/react'; -import React, { useState } from 'react'; - -import { useDeleteManyMutation, useQuery } from '../../src'; -import type { Database } from '../database.types'; -import { renderWithConfig } from '../utils'; - -const TEST_PREFIX = 'postgrest-react-query-delmany'; - -describe('useDeleteManyMutation', () => { - let client: SupabaseClient; - let testRunPrefix: string; - - let contacts: Database['public']['Tables']['contact']['Row'][]; - - beforeAll(async () => { - testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; - client = createClient( - process.env.SUPABASE_URL as string, - process.env.SUPABASE_ANON_KEY as string, - ); - }); - - beforeEach(async () => { - await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); - - const { data } = await client - .from('contact') - .insert( - new Array(3) - .fill(0) - .map((idx) => ({ username: `${testRunPrefix}-${idx}` })), - ) - .select('*'); - contacts = data as Database['public']['Tables']['contact']['Row'][]; - }); - - it('should delete existing cache item and reduce count', async () => { - const queryClient = new QueryClient(); - function Page() { - const [success, setSuccess] = useState(false); - const { data, count } = useQuery( - client - .from('contact') - .select('id,username', { count: 'exact' }) - .eq('username', contacts[0].username ?? ''), - ); - const { mutateAsync: deleteContact } = useDeleteManyMutation( - client.from('contact'), - ['id'], - null, - { onSuccess: () => setSuccess(true) }, - ); - const { mutateAsync: deleteWithEmptyOptions } = useDeleteManyMutation( - client.from('contact'), - ['id'], - null, - {}, - ); - const { mutateAsync: deleteWithoutOptions } = useDeleteManyMutation( - client.from('contact'), - ['id'], - ); - return ( -
-
- await deleteContact([ - { - id: (data ?? []).find((c) => c)?.id, - }, - ]) - } - /> -
- await deleteWithEmptyOptions([ - { - id: (data ?? []).find((c) => c)?.id, - }, - ]) - } - /> -
- await deleteWithoutOptions([ - { - id: (data ?? []).find((c) => c)?.id, - }, - ]) - } - /> - {(data ?? []).map((d) => ( - {d.username} - ))} - {`count: ${count}`} - {`success: ${success}`} -
- ); - } - - renderWithConfig(, queryClient); - await screen.findByText( - `count: ${contacts.length}`, - {}, - { timeout: 10000 }, - ); - fireEvent.click(screen.getByTestId('deleteWithEmptyOptions')); - await screen.findByText( - `count: ${contacts.length - 1}`, - {}, - { timeout: 10000 }, - ); - fireEvent.click(screen.getByTestId('deleteWithoutOptions')); - await screen.findByText( - `count: ${contacts.length - 2}`, - {}, - { timeout: 10000 }, - ); - fireEvent.click(screen.getByTestId('delete')); - await screen.findByText('success: true', {}, { timeout: 10000 }); - await screen.findByText( - `count: ${contacts.length - 3}`, - {}, - { timeout: 10000 }, - ); - }); -}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.ts b/packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.ts new file mode 100644 index 00000000..487b4f83 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.ts @@ -0,0 +1,113 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/vue-query'; +import { fireEvent, screen } from '@testing-library/vue'; + +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; +import DeleteMutationPage1 from '../components/DeleteMutationPage-1.vue'; +import DeleteMutationPage2 from '../components/DeleteMutationPage-2.vue'; + +const TEST_PREFIX = 'postgrest-vue-query-delete'; + +describe('useDeleteMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + let contacts: Database['public']['Tables']['contact']['Row'][]; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + }); + + beforeEach(async () => { + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + + const { data } = await client + .from('contact') + .insert( + new Array(3) + .fill(0) + .map((idx) => ({ username: `${testRunPrefix}-${idx}` })), + ) + .select('*'); + contacts = data as Database['public']['Tables']['contact']['Row'][]; + }); + + it('should invalidate address_book cache after delete', async () => { + const { data: addressBooks } = await client + .from('address_book') + .insert([ + { + name: 'hello', + }, + ]) + .select('id'); + + const addressBookId = addressBooks ? addressBooks[0].id : ''; + + await client.from('address_book_contact').insert([ + { + address_book: addressBookId, + contact: contacts[0].id, + }, + { + address_book: addressBookId, + contact: contacts[1].id, + }, + ]); + + const queryClient = new QueryClient(); + + renderWithConfig( + DeleteMutationPage1, + { client, addressBookId }, + queryClient, + ); + + await screen.findByText(`hello`, {}, { timeout: 10000 }); + + await screen.findByText(`count: 2`, {}, { timeout: 10000 }); + + const deleteButtons = screen.getAllByRole(`button`, { + name: /Delete Contact/i, + }); + + fireEvent.click(deleteButtons[0]); + + await screen.findByText(`count: 1`, {}, { timeout: 10000 }); + }); + + it('should delete existing cache item and reduce count', async () => { + const queryClient = new QueryClient(); + + renderWithConfig(DeleteMutationPage2, {}, queryClient); + await screen.findByText( + `count: ${contacts.length}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('deleteWithEmptyOptions')); + await screen.findByText( + `count: ${contacts.length - 1}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('deleteWithoutOptions')); + await screen.findByText( + `count: ${contacts.length - 2}`, + {}, + { timeout: 10000 }, + ); + fireEvent.click(screen.getByTestId('delete')); + await screen.findByText('success: true', {}, { timeout: 10000 }); + await screen.findByText( + `count: ${contacts.length - 3}`, + {}, + { timeout: 10000 }, + ); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.tsx deleted file mode 100644 index 43a02694..00000000 --- a/packages/postgrest-vue-query/__tests__/mutate/use-delete-mutation.integration.spec.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; -import { fireEvent, screen } from '@testing-library/react'; -import React, { useState } from 'react'; - -import { useDeleteMutation, useQuery } from '../../src'; -import type { Database } from '../database.types'; -import { renderWithConfig } from '../utils'; - -const TEST_PREFIX = 'postgrest-react-query-delete'; - -describe('useDeleteMutation', () => { - let client: SupabaseClient; - let testRunPrefix: string; - - let contacts: Database['public']['Tables']['contact']['Row'][]; - - beforeAll(async () => { - testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; - client = createClient( - process.env.SUPABASE_URL as string, - process.env.SUPABASE_ANON_KEY as string, - ); - }); - - beforeEach(async () => { - await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); - - const { data } = await client - .from('contact') - .insert( - new Array(3) - .fill(0) - .map((idx) => ({ username: `${testRunPrefix}-${idx}` })), - ) - .select('*'); - contacts = data as Database['public']['Tables']['contact']['Row'][]; - }); - - it('should invalidate address_book cache after delete', async () => { - const { data: addressBooks } = await client - .from('address_book') - .insert([ - { - name: 'hello', - }, - ]) - .select('id'); - - const addressBookId = addressBooks ? addressBooks[0].id : ''; - - await client.from('address_book_contact').insert([ - { - address_book: addressBookId, - contact: contacts[0].id, - }, - { - address_book: addressBookId, - contact: contacts[1].id, - }, - ]); - - const queryClient = new QueryClient(); - function Page() { - const { data: addressBookAndContact } = useQuery( - client - .from('address_book') - .select('id, name, contacts:contact (id, username)') - .eq('id', addressBookId) - .single(), - ); - - const { mutateAsync: deleteContactFromAddressBook } = useDeleteMutation( - client.from('address_book_contact'), - ['contact', 'address_book'], - 'contact, address_book', - { - revalidateRelations: [ - { - relation: 'address_book', - relationIdColumn: 'id', - fKeyColumn: 'address_book', - }, - ], - }, - ); - - return ( -
- {addressBookAndContact?.name} - - count: {addressBookAndContact?.contacts.length} - - {addressBookAndContact?.contacts.map((contact) => { - return ( -
- {contact.username} - -
- ); - })} -
- ); - } - - renderWithConfig(, queryClient); - - await screen.findByText(`hello`, {}, { timeout: 10000 }); - - await screen.findByText(`count: 2`, {}, { timeout: 10000 }); - - const deleteButtons = screen.getAllByRole(`button`, { - name: /Delete Contact/i, - }); - - fireEvent.click(deleteButtons[0]); - - await screen.findByText(`count: 1`, {}, { timeout: 10000 }); - }); - - it('should delete existing cache item and reduce count', async () => { - const queryClient = new QueryClient(); - function Page() { - const [success, setSuccess] = useState(false); - const { data, count } = useQuery( - client - .from('contact') - .select('id,username', { count: 'exact' }) - .eq('username', contacts[0].username ?? ''), - ); - const { mutateAsync: deleteContact } = useDeleteMutation( - client.from('contact'), - ['id'], - null, - { onSuccess: () => setSuccess(true) }, - ); - const { mutateAsync: deleteWithEmptyOptions } = useDeleteMutation( - client.from('contact'), - ['id'], - null, - {}, - ); - const { mutateAsync: deleteWithoutOptions } = useDeleteMutation( - client.from('contact'), - ['id'], - ); - return ( -
-
- await deleteContact({ - id: (data ?? []).find((c) => c)?.id, - }) - } - /> -
- await deleteWithEmptyOptions({ - id: (data ?? []).find((c) => c)?.id, - }) - } - /> -
- await deleteWithoutOptions({ - id: (data ?? []).find((c) => c)?.id, - }) - } - /> - {(data ?? []).map((d) => ( - {d.username} - ))} - {`count: ${count}`} - {`success: ${success}`} -
- ); - } - - renderWithConfig(, queryClient); - await screen.findByText( - `count: ${contacts.length}`, - {}, - { timeout: 10000 }, - ); - fireEvent.click(screen.getByTestId('deleteWithEmptyOptions')); - await screen.findByText( - `count: ${contacts.length - 1}`, - {}, - { timeout: 10000 }, - ); - fireEvent.click(screen.getByTestId('deleteWithoutOptions')); - await screen.findByText( - `count: ${contacts.length - 2}`, - {}, - { timeout: 10000 }, - ); - fireEvent.click(screen.getByTestId('delete')); - await screen.findByText('success: true', {}, { timeout: 10000 }); - await screen.findByText( - `count: ${contacts.length - 3}`, - {}, - { timeout: 10000 }, - ); - }); -}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.ts b/packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.ts new file mode 100644 index 00000000..2ca986b3 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.ts @@ -0,0 +1,47 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/vue-query'; +import { fireEvent, screen } from '@testing-library/vue'; + +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; +import Page from '../components/InsertMutationPage.vue'; + +const TEST_PREFIX = 'postgrest-vue-query-insert'; + +describe('useInsertMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + it('should insert into existing cache item with alias', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-1`; + const USERNAME_2 = `${testRunPrefix}-2`; + const USERNAME_3 = `${testRunPrefix}-3`; + + renderWithConfig( + Page, + { + client, + userName1: USERNAME_1, + userName2: USERNAME_2, + userName3: USERNAME_3, + }, + queryClient, + ); + await screen.findByText('count: 0', {}, { timeout: 10000 }); + fireEvent.click(screen.getByTestId('insertMany')); + await screen.findByText(USERNAME_2, {}, { timeout: 10000 }); + await screen.findByText(USERNAME_3, {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 2'); + await screen.findByText('success: true', {}, { timeout: 10000 }); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.tsx deleted file mode 100644 index e13f47c5..00000000 --- a/packages/postgrest-vue-query/__tests__/mutate/use-insert-mutation.integration.spec.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; -import { fireEvent, screen } from '@testing-library/react'; -import React, { useState } from 'react'; - -import { useInsertMutation, useQuery } from '../../src'; -import type { Database } from '../database.types'; -import { renderWithConfig } from '../utils'; - -const TEST_PREFIX = 'postgrest-react-query-insert'; - -describe('useInsertMutation', () => { - let client: SupabaseClient; - let testRunPrefix: string; - - beforeAll(async () => { - testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; - client = createClient( - process.env.SUPABASE_URL as string, - process.env.SUPABASE_ANON_KEY as string, - ); - await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); - }); - - it('should insert into existing cache item with alias', async () => { - const queryClient = new QueryClient(); - const USERNAME_1 = `${testRunPrefix}-1`; - const USERNAME_2 = `${testRunPrefix}-2`; - const USERNAME_3 = `${testRunPrefix}-3`; - function Page() { - const [success, setSuccess] = useState(false); - const { data, count } = useQuery( - client - .from('contact') - .select('id,alias:username', { count: 'exact' }) - .in('username', [USERNAME_1, USERNAME_2, USERNAME_3]), - ); - const { mutateAsync: insert } = useInsertMutation( - client.from('contact'), - ['id'], - null, - { - onSuccess: () => setSuccess(true), - }, - ); - - return ( -
-
- await insert([ - { - username: USERNAME_2, - }, - { - username: USERNAME_3, - }, - ]) - } - /> - {(data ?? []).map((d) => ( - {d.alias} - ))} - {`count: ${count}`} - {`success: ${success}`} -
- ); - } - - renderWithConfig(, queryClient); - await screen.findByText('count: 0', {}, { timeout: 10000 }); - fireEvent.click(screen.getByTestId('insertMany')); - await screen.findByText(USERNAME_2, {}, { timeout: 10000 }); - await screen.findByText(USERNAME_3, {}, { timeout: 10000 }); - expect(screen.getByTestId('count').textContent).toEqual('count: 2'); - await screen.findByText('success: true', {}, { timeout: 10000 }); - }); -}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.ts b/packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.ts new file mode 100644 index 00000000..fdf7101f --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.ts @@ -0,0 +1,43 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/vue-query'; +import { fireEvent, screen } from '@testing-library/vue'; + +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; +import Page from '../components/UpdateMutationPage.vue'; + +const TEST_PREFIX = 'postgrest-react-query-update'; + +describe('useUpdateMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + it('should update existing cache item', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-2`; + const USERNAME_2 = `${testRunPrefix}-3`; + + renderWithConfig( + Page, + { client, username1: USERNAME_1, username2: USERNAME_2 }, + queryClient, + ); + await screen.findByText('count: 0', {}, { timeout: 10000 }); + fireEvent.click(screen.getByTestId('insert')); + await screen.findByText(USERNAME_1, {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + fireEvent.click(screen.getByTestId('update')); + await screen.findByText(USERNAME_2, {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + await screen.findByText('success: true', {}, { timeout: 10000 }); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.tsx deleted file mode 100644 index da01a5f9..00000000 --- a/packages/postgrest-vue-query/__tests__/mutate/use-update-mutation.integration.spec.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; -import { fireEvent, screen } from '@testing-library/react'; -import React, { useState } from 'react'; - -import { useInsertMutation, useQuery, useUpdateMutation } from '../../src'; -import type { Database } from '../database.types'; -import { renderWithConfig } from '../utils'; - -const TEST_PREFIX = 'postgrest-react-query-update'; - -describe('useUpdateMutation', () => { - let client: SupabaseClient; - let testRunPrefix: string; - - beforeAll(async () => { - testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; - client = createClient( - process.env.SUPABASE_URL as string, - process.env.SUPABASE_ANON_KEY as string, - ); - await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); - }); - - it('should update existing cache item', async () => { - const queryClient = new QueryClient(); - const USERNAME_1 = `${testRunPrefix}-2`; - const USERNAME_2 = `${testRunPrefix}-3`; - function Page() { - const [success, setSuccess] = useState(false); - const { data, count } = useQuery( - client - .from('contact') - .select('id,username', { count: 'exact' }) - .in('username', [USERNAME_1, USERNAME_2]), - ); - const { mutateAsync: insert } = useInsertMutation( - client.from('contact'), - ['id'], - ); - const { mutateAsync: update } = useUpdateMutation( - client.from('contact'), - ['id'], - null, - { - onSuccess: () => setSuccess(true), - }, - ); - return ( -
-
await insert([{ username: USERNAME_1 }])} - /> -
- await update({ - id: (data ?? []).find((d) => d.username === USERNAME_1)?.id, - username: USERNAME_2, - }) - } - /> - - { - data?.find((d) => - [USERNAME_1, USERNAME_2].includes(d.username ?? ''), - )?.username - } - - {`count: ${count}`} - {`success: ${success}`} -
- ); - } - - renderWithConfig(, queryClient); - await screen.findByText('count: 0', {}, { timeout: 10000 }); - fireEvent.click(screen.getByTestId('insert')); - await screen.findByText(USERNAME_1, {}, { timeout: 10000 }); - expect(screen.getByTestId('count').textContent).toEqual('count: 1'); - fireEvent.click(screen.getByTestId('update')); - await screen.findByText(USERNAME_2, {}, { timeout: 10000 }); - expect(screen.getByTestId('count').textContent).toEqual('count: 1'); - await screen.findByText('success: true', {}, { timeout: 10000 }); - }); -}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.ts b/packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.ts new file mode 100644 index 00000000..5252c915 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.ts @@ -0,0 +1,48 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/vue-query'; +import { fireEvent, screen } from '@testing-library/vue'; + +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; +import Page from '../components/UpsertMutationPage.vue'; + +const TEST_PREFIX = 'postgrest-react-query-upsert'; + +describe('useUpsertMutation', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + it('should upsert into existing cache item', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-2`; + const USERNAME_2 = `${testRunPrefix}-3`; + + await client + .from('contact') + .insert({ + username: USERNAME_1, + golden_ticket: true, + }) + .throwOnError(); + renderWithConfig( + Page, + { client, username1: USERNAME_1, username2: USERNAME_2 }, + queryClient, + ); + await screen.findByText('count: 1', {}, { timeout: 10000 }); + fireEvent.click(screen.getByTestId('upsertMany')); + await screen.findByText(`${USERNAME_1} - true`, {}, { timeout: 10000 }); + await screen.findByText(`${USERNAME_2} - null`, {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 2'); + await screen.findByText('success: true', {}, { timeout: 10000 }); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.tsx deleted file mode 100644 index 1c4ef019..00000000 --- a/packages/postgrest-vue-query/__tests__/mutate/use-upsert-mutation.integration.spec.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; -import { fireEvent, screen } from '@testing-library/react'; -import React, { useState } from 'react'; - -import { useQuery, useUpsertMutation } from '../../src'; -import type { Database } from '../database.types'; -import { renderWithConfig } from '../utils'; - -const TEST_PREFIX = 'postgrest-react-query-upsert'; - -describe('useUpsertMutation', () => { - let client: SupabaseClient; - let testRunPrefix: string; - - beforeAll(async () => { - testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; - client = createClient( - process.env.SUPABASE_URL as string, - process.env.SUPABASE_ANON_KEY as string, - ); - await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); - }); - - it('should upsert into existing cache item', async () => { - const queryClient = new QueryClient(); - const USERNAME_1 = `${testRunPrefix}-2`; - const USERNAME_2 = `${testRunPrefix}-3`; - function Page() { - const [success, setSuccess] = useState(false); - const { data, count } = useQuery( - client - .from('contact') - .select('id,username,golden_ticket', { count: 'exact' }) - .in('username', [USERNAME_1, USERNAME_2]), - ); - - const { mutateAsync: upsert } = useUpsertMutation( - client.from('contact'), - ['id'], - null, - { - onSuccess: () => setSuccess(true), - }, - ); - - return ( -
-
- await upsert([ - { - id: data?.find((d) => d.username === USERNAME_1)?.id, - username: USERNAME_1, - golden_ticket: true, - }, - { - id: 'cae53d23-51a8-4408-9f40-05c83a4b0bbd', - username: USERNAME_2, - golden_ticket: null, - }, - ]) - } - /> - {(data ?? []).map((d) => ( - - {`${d.username} - ${d.golden_ticket ?? 'null'}`} - - ))} - {`count: ${count}`} - {`success: ${success}`} -
- ); - } - - await client - .from('contact') - .insert({ - username: USERNAME_1, - golden_ticket: true, - }) - .throwOnError(); - renderWithConfig(, queryClient); - await screen.findByText('count: 1', {}, { timeout: 10000 }); - fireEvent.click(screen.getByTestId('upsertMany')); - await screen.findByText(`${USERNAME_1} - true`, {}, { timeout: 10000 }); - await screen.findByText(`${USERNAME_2} - null`, {}, { timeout: 10000 }); - expect(screen.getByTestId('count').textContent).toEqual('count: 2'); - await screen.findByText('success: true', {}, { timeout: 10000 }); - }); -}); diff --git a/packages/postgrest-vue-query/__tests__/query/fetch.spec.ts b/packages/postgrest-vue-query/__tests__/query/fetch.spec.ts index 0e46238e..240b3442 100644 --- a/packages/postgrest-vue-query/__tests__/query/fetch.spec.ts +++ b/packages/postgrest-vue-query/__tests__/query/fetch.spec.ts @@ -1,11 +1,11 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; +import { QueryClient } from '@tanstack/vue-query'; import { fetchQuery } from '../../src'; import type { Database } from '../database.types'; import '../utils'; -const TEST_PREFIX = 'postgrest-react-query-fetch'; +const TEST_PREFIX = 'postgrest-vue-query-fetch'; describe('fetchQuery', () => { let client: SupabaseClient; diff --git a/packages/postgrest-vue-query/__tests__/query/prefetch.integration.spec.ts b/packages/postgrest-vue-query/__tests__/query/prefetch.integration.spec.ts index feaef48b..41a65a3a 100644 --- a/packages/postgrest-vue-query/__tests__/query/prefetch.integration.spec.ts +++ b/packages/postgrest-vue-query/__tests__/query/prefetch.integration.spec.ts @@ -1,11 +1,11 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; +import { QueryClient } from '@tanstack/vue-query'; import { fetchQueryInitialData, prefetchQuery } from '../../src'; import type { Database } from '../database.types'; import '../utils'; -const TEST_PREFIX = 'postgrest-react-query-prefetch'; +const TEST_PREFIX = 'postgrest-vue-query-prefetch'; describe('prefetch', () => { let client: SupabaseClient; diff --git a/packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.ts similarity index 54% rename from packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.tsx rename to packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.ts index 1b38cbe9..8e1b0561 100644 --- a/packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.tsx +++ b/packages/postgrest-vue-query/__tests__/query/use-query.integration.spec.ts @@ -1,14 +1,20 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; -import { fireEvent, screen } from '@testing-library/react'; -import React, { useState } from 'react'; +import { QueryClient } from '@tanstack/vue-query'; +import { fireEvent, screen } from '@testing-library/vue'; -import { fetchQueryInitialData, prefetchQuery, useQuery } from '../../src'; +import { fetchQueryInitialData, prefetchQuery } from '../../src'; import { encode } from '../../src/lib/key'; import type { Database } from '../database.types'; import { renderWithConfig } from '../utils'; +import QueryPage1 from '../components/QueryPage-1.vue'; +import QueryPage2 from '../components/QueryPage-2.vue'; +import QueryPage3 from '../components/QueryPage-3.vue'; +import QueryPage4 from '../components/QueryPage-4.vue'; +import QueryPage5 from '../components/QueryPage-5.vue'; +import QueryPage6 from '../components/QueryPage-6.vue'; +import QueryPage7 from '../components/QueryPage-7.vue'; -const TEST_PREFIX = 'postgrest-react-query-query'; +const TEST_PREFIX = 'postgrest-vue-query-query'; describe('useQuery', () => { let client: SupabaseClient; @@ -44,13 +50,8 @@ describe('useQuery', () => { .select('id,username') .eq('username', contacts[0].username ?? '') .single(); - function Page() { - const { data } = useQuery(query); - return
{data?.username}
; - } - - renderWithConfig(, queryClient); + renderWithConfig(QueryPage1, { client, query }, queryClient); await screen.findByText( contacts[0].username as string, {}, @@ -66,14 +67,8 @@ describe('useQuery', () => { .select('id,username') .eq('username', 'unknown') .maybeSingle(); - function Page() { - const { data, isLoading } = useQuery(query); - return ( -
{isLoading ? 'validating' : `username: ${data?.username}`}
- ); - } - - renderWithConfig(, queryClient); + + renderWithConfig(QueryPage2, { client, query }, queryClient); await screen.findByText('username: undefined', {}, { timeout: 10000 }); expect(queryClient.getQueryData(encode(query, false))).toBeDefined(); }); @@ -84,22 +79,8 @@ describe('useQuery', () => { .from('contact') .select('id,username', { count: 'exact' }) .ilike('username', `${testRunPrefix}%`); - function Page() { - const { data, count } = useQuery(query); - return ( -
-
- { - (data ?? []).find((d) => d.username === contacts[0].username) - ?.username - } -
-
{count}
-
- ); - } - - renderWithConfig(, queryClient); + + renderWithConfig(QueryPage3, { client, query, contacts }, queryClient); await screen.findByText( contacts[0].username as string, {}, @@ -111,27 +92,8 @@ describe('useQuery', () => { it('should work for with conditional query', async () => { const queryClient = new QueryClient(); - function Page() { - const [condition, setCondition] = useState(false); - const { data, isLoading } = useQuery( - client - .from('contact') - .select('id,username') - .eq('username', contacts[0].username ?? '') - .maybeSingle(), - { enabled: condition }, - ); - - return ( -
-
setCondition(true)} /> -
{data?.username ?? 'undefined'}
-
{`isLoading: ${isLoading}`}
-
- ); - } - - renderWithConfig(, queryClient); + + renderWithConfig(QueryPage4, { client, contacts }, queryClient); await screen.findByText('undefined', {}, { timeout: 10000 }); fireEvent.click(screen.getByTestId('setCondition')); await screen.findByText( @@ -143,32 +105,8 @@ describe('useQuery', () => { it('refetch should work', async () => { const queryClient = new QueryClient(); - function Page() { - const { data, refetch, isLoading } = useQuery( - client - .from('contact') - .select('id,username') - .eq('username', contacts[0].username ?? '') - .single(), - ); - const [refetched, setRefetched] = useState(null); - - return ( -
-
{ - setRefetched((await refetch())?.data?.data); - }} - /> -
{data?.username ?? 'undefined'}
-
{`refetched: ${!!refetched}`}
-
{`isLoading: ${isLoading}`}
-
- ); - } - - renderWithConfig(, queryClient); + + renderWithConfig(QueryPage5, { client, contacts }, queryClient); await screen.findByText('isLoading: false', {}, { timeout: 10000 }); fireEvent.click(screen.getByTestId('mutate')); await screen.findByText('refetched: true', {}, { timeout: 10000 }); @@ -182,20 +120,14 @@ describe('useQuery', () => { .eq('username', contacts[0].username ?? '') .single(); await prefetchQuery(queryClient, query); - let hasBeenFalse = false; - function Page() { - const { data } = useQuery(query); - if (!data) hasBeenFalse = true; - - return ( -
-
{data?.username ?? 'undefined'}
-
- ); - } - - renderWithConfig(, queryClient); - expect(hasBeenFalse).toBe(false); + + const wrapper = renderWithConfig( + QueryPage6, + { client, query }, + queryClient, + ); + const updateEvent = wrapper.emitted('update'); + expect(updateEvent).toHaveLength(0); await screen.findByText(contacts[0].username!, {}, { timeout: 10000 }); }); @@ -206,21 +138,15 @@ describe('useQuery', () => { .select('id,username') .eq('username', contacts[0].username ?? '') .single(); - const [key, initial] = await fetchQueryInitialData(query); - let hasBeenFalse = false; - function Page() { - const { data } = useQuery(query, { initialData: initial }); - if (!data) hasBeenFalse = true; - - return ( -
-
{data?.username ?? 'undefined'}
-
- ); - } - - renderWithConfig(, queryClient); - expect(hasBeenFalse).toBe(false); + const [_, initial] = await fetchQueryInitialData(query); + + const wrapper = renderWithConfig( + QueryPage7, + { client, query, initial }, + queryClient, + ); + const updateEvent = wrapper.emitted('update'); + expect(updateEvent).toHaveLength(0); await screen.findByText(contacts[0].username!, {}, { timeout: 10000 }); }); }); diff --git a/packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query-integration.spec.tsx b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query-integration.spec.tsx deleted file mode 100644 index a5fde1c1..00000000 --- a/packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query-integration.spec.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; -import { act, screen } from '@testing-library/react'; -import React, { useState } from 'react'; - -import { useSubscriptionQuery, useQuery } from '../../src'; -import type { Database } from '../database.types'; -import { renderWithConfig } from '../utils'; - -const TEST_PREFIX = 'postgrest-react-query-subscription-query'; - -describe('useSubscriptionQuery', () => { - let client: SupabaseClient; - let testRunPrefix: string; - - beforeAll(async () => { - testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; - client = createClient( - process.env.SUPABASE_URL as string, - process.env.SUPABASE_ANON_KEY as string, - ); - await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); - }); - - afterEach(async () => { - if (client) await client.removeAllChannels(); - }); - - it('should properly update cache', async () => { - const queryClient = new QueryClient(); - const USERNAME_1 = `${testRunPrefix}-1`; - function Page() { - const { data, count } = useQuery( - client - .from('contact') - .select('id,username,has_low_ticket_number,ticket_number', { - count: 'exact', - }) - .eq('username', USERNAME_1), - ); - - const [cbCalled, setCbCalled] = useState(false); - - const { status } = useSubscriptionQuery( - client, - `public:contact:username=eq.${USERNAME_1}`, - { - event: '*', - table: 'contact', - schema: 'public', - filter: `username=eq.${USERNAME_1}`, - }, - ['id'], - 'id,username,has_low_ticket_number,ticket_number', - { - callback: (evt) => { - if (evt.data.ticket_number === 1000) { - setCbCalled(true); - } - }, - }, - ); - - return ( -
- {(data ?? []).map((d) => ( - {`ticket_number: ${d.ticket_number} | has_low_ticket_number: ${d.has_low_ticket_number}`} - ))} - {`count: ${count}`} - {status} - {`cbCalled: ${cbCalled}`} -
- ); - } - - const { unmount } = renderWithConfig(, queryClient); - await screen.findByText('count: 0', {}, { timeout: 10000 }); - await screen.findByText('SUBSCRIBED', {}, { timeout: 10000 }); - await new Promise((resolve) => setTimeout(resolve, 2000)); - await act(async () => { - await client - .from('contact') - .insert({ username: USERNAME_1, ticket_number: 1 }) - .select('*') - .throwOnError() - .single(); - }); - await screen.findByText( - 'ticket_number: 1 | has_low_ticket_number: true', - {}, - { timeout: 10000 }, - ); - expect(screen.getByTestId('count').textContent).toEqual('count: 1'); - await act(async () => { - await client - .from('contact') - .update({ ticket_number: 1000 }) - .eq('username', USERNAME_1) - .throwOnError(); - }); - await screen.findByText( - 'ticket_number: 1000 | has_low_ticket_number: false', - {}, - { timeout: 10000 }, - ); - expect(screen.getByTestId('count').textContent).toEqual('count: 1'); - await screen.findByText('cbCalled: true', {}, { timeout: 10000 }); - unmount(); - }); -}); diff --git a/packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query.integration.spec.ts b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query.integration.spec.ts new file mode 100644 index 00000000..829234d2 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription-query.integration.spec.ts @@ -0,0 +1,70 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/vue-query'; +import { screen } from '@testing-library/vue'; + +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; +import Page from '../components/SubscriptionQueryPage.vue'; + +const TEST_PREFIX = 'postgrest-vue-query-subscription-query'; + +describe('useSubscriptionQuery', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + afterEach(async () => { + if (client) await client.removeAllChannels(); + }); + + it('should properly update cache', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-1`; + + const { unmount } = renderWithConfig( + Page, + { client, username: USERNAME_1 }, + queryClient, + ); + await screen.findByText('count: 0', {}, { timeout: 10000 }); + await screen.findByText('SUBSCRIBED', {}, { timeout: 10000 }); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + await client + .from('contact') + .insert({ username: USERNAME_1, ticket_number: 1 }) + .select('*') + .throwOnError() + .single(); + + await screen.findByText( + 'ticket_number: 1 | has_low_ticket_number: true', + {}, + { timeout: 10000 }, + ); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + + await client + .from('contact') + .update({ ticket_number: 1000 }) + .eq('username', USERNAME_1) + .throwOnError(); + + await screen.findByText( + 'ticket_number: 1000 | has_low_ticket_number: false', + {}, + { timeout: 10000 }, + ); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + await screen.findByText('cbCalled: true', {}, { timeout: 10000 }); + unmount(); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.ts b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.ts new file mode 100644 index 00000000..0c576eb0 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.ts @@ -0,0 +1,61 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { QueryClient } from '@tanstack/vue-query'; +import { screen } from '@testing-library/vue'; + +import type { Database } from '../database.types'; +import { renderWithConfig } from '../utils'; +import Page from '../components/SubscriptionPage.vue'; + +const TEST_PREFIX = 'postgrest-vue-query-subscription-plain'; + +describe('useSubscription', () => { + let client: SupabaseClient; + let testRunPrefix: string; + + beforeAll(async () => { + testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; + client = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_ANON_KEY as string, + ); + await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); + }); + + afterEach(async () => { + if (client) await client.removeAllChannels(); + }); + + it('should properly update cache', async () => { + const queryClient = new QueryClient(); + const USERNAME_1 = `${testRunPrefix}-1`; + + const { unmount } = renderWithConfig( + Page, + { client, username: USERNAME_1 }, + queryClient, + ); + await screen.findByText('count: 0', {}, { timeout: 10000 }); + await screen.findByText('SUBSCRIBED', {}, { timeout: 10000 }); + + await client + .from('contact') + .insert({ username: USERNAME_1, ticket_number: 1 }) + .select('id') + .throwOnError() + .single(); + + await screen.findByText('ticket_number: 1', {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + + await client + .from('contact') + .update({ ticket_number: 5 }) + .eq('username', USERNAME_1) + .throwOnError(); + + await screen.findByText('ticket_number: 5', {}, { timeout: 10000 }); + expect(screen.getByTestId('count').textContent).toEqual('count: 1'); + await screen.findByText('cbCalled: true', {}, { timeout: 10000 }); + unmount(); + }); +}); diff --git a/packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.tsx b/packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.tsx deleted file mode 100644 index 5e1b5709..00000000 --- a/packages/postgrest-vue-query/__tests__/subscribe/use-subscription.integration.spec.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient } from '@tanstack/react-query'; -import { act, screen } from '@testing-library/react'; -import React, { useState } from 'react'; - -import { useSubscription, useQuery } from '../../src'; -import type { Database } from '../database.types'; -import { renderWithConfig } from '../utils'; - -const TEST_PREFIX = 'postgrest-react-query-subscription-plain'; - -describe('useSubscription', () => { - let client: SupabaseClient; - let testRunPrefix: string; - - beforeAll(async () => { - testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; - client = createClient( - process.env.SUPABASE_URL as string, - process.env.SUPABASE_ANON_KEY as string, - ); - await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); - }); - - afterEach(async () => { - if (client) await client.removeAllChannels(); - }); - - it('should properly update cache', async () => { - const queryClient = new QueryClient(); - const USERNAME_1 = `${testRunPrefix}-1`; - function Page() { - const { data, count } = useQuery( - client - .from('contact') - .select('id,username,ticket_number', { count: 'exact' }) - .eq('username', USERNAME_1), - ); - - const [cbCalled, setCbCalled] = useState(false); - - const { status } = useSubscription( - client, - `public:contact:username=eq.${USERNAME_1}`, - { - event: '*', - table: 'contact', - schema: 'public', - filter: `username=eq.${USERNAME_1}`, - }, - ['id'], - { callback: () => setCbCalled(true) }, - ); - - return ( -
- {(data ?? []).map((d) => ( - {`ticket_number: ${d.ticket_number}`} - ))} - {`count: ${count}`} - {status} - {`cbCalled: ${cbCalled}`} -
- ); - } - - const { unmount } = renderWithConfig(, queryClient); - await screen.findByText('count: 0', {}, { timeout: 10000 }); - await screen.findByText('SUBSCRIBED', {}, { timeout: 10000 }); - await act(async () => { - await client - .from('contact') - .insert({ username: USERNAME_1, ticket_number: 1 }) - .select('id') - .throwOnError() - .single(); - }); - await screen.findByText('ticket_number: 1', {}, { timeout: 10000 }); - expect(screen.getByTestId('count').textContent).toEqual('count: 1'); - await act(async () => { - await client - .from('contact') - .update({ ticket_number: 5 }) - .eq('username', USERNAME_1) - .throwOnError(); - }); - await screen.findByText('ticket_number: 5', {}, { timeout: 10000 }); - expect(screen.getByTestId('count').textContent).toEqual('count: 1'); - await screen.findByText('cbCalled: true', {}, { timeout: 10000 }); - unmount(); - }); -}); diff --git a/packages/postgrest-vue-query/__tests__/utils.ts b/packages/postgrest-vue-query/__tests__/utils.ts new file mode 100644 index 00000000..230ed2e5 --- /dev/null +++ b/packages/postgrest-vue-query/__tests__/utils.ts @@ -0,0 +1,20 @@ +import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'; +import { render } from '@testing-library/vue'; +import * as dotenv from 'dotenv'; +import { resolve } from 'node:path'; + +dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); + +export const renderWithConfig = ( + element: any, + props?: { [key: string]: unknown }, + queryClient?: QueryClient, +): ReturnType => { + const client = queryClient ?? new QueryClient(); + return render(element, { + props, + global: { + plugins: [[VueQueryPlugin, { queryClient: client }]], + }, + }); +}; diff --git a/packages/postgrest-vue-query/__tests__/utils.tsx b/packages/postgrest-vue-query/__tests__/utils.tsx deleted file mode 100644 index 8f0cadb4..00000000 --- a/packages/postgrest-vue-query/__tests__/utils.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render } from '@testing-library/react'; -import * as dotenv from 'dotenv'; -import { resolve } from 'node:path'; -import React from 'react'; - -dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); - -export const renderWithConfig = ( - element: React.ReactElement, - queryClient: QueryClient, -): ReturnType => { - const TestQueryClientProvider = ({ - children, - }: { - children: React.ReactNode; - }) => ( - {children} - ); - return render(element, { wrapper: TestQueryClientProvider }); -}; diff --git a/packages/postgrest-vue-query/package.json b/packages/postgrest-vue-query/package.json index 8f37a493..6a0fc33a 100644 --- a/packages/postgrest-vue-query/package.json +++ b/packages/postgrest-vue-query/package.json @@ -48,32 +48,33 @@ "directory": "packages/postgrest-vue-query" }, "peerDependencies": { + "@supabase/postgrest-js": "^1.9.0", "@tanstack/vue-query": "^5.28.13", - "vue": "^3.4.21", - "@supabase/postgrest-js": "^1.9.0" + "vue": "^3.4.21" }, "jest": { "preset": "@supabase-cache-helpers/jest-presets/jest/node" }, "devDependencies": { - "@supabase/supabase-js": "2.38.5", + "@supabase-cache-helpers/eslint-config-custom": "workspace:*", + "@supabase-cache-helpers/jest-presets": "workspace:*", + "@supabase-cache-helpers/prettier-config": "workspace:*", + "@supabase-cache-helpers/tsconfig": "workspace:*", "@supabase/postgrest-js": "1.9.0", - "@testing-library/vue": "8.0.3", + "@supabase/supabase-js": "2.38.5", "@testing-library/jest-dom": "6.4.0", - "jest-environment-jsdom": "29.7.0", + "@testing-library/vue": "8.0.3", + "@types/flat": "5.0.2", "@types/jest": "29.5.0", "dotenv": "16.4.0", "eslint": "8.54.0", - "@supabase-cache-helpers/eslint-config-custom": "workspace:*", - "@supabase-cache-helpers/prettier-config": "workspace:*", + "eslint-plugin-vue": "^9.25.0", "jest": "29.7.0", - "@supabase-cache-helpers/jest-presets": "workspace:*", + "jest-environment-jsdom": "29.7.0", "ts-jest": "29.1.0", - "@supabase-cache-helpers/tsconfig": "workspace:*", - "@types/flat": "5.0.2", "tsup": "8.0.0", - "vue": "3.4.21", - "typescript": "5.4.2" + "typescript": "5.4.2", + "vue": "3.4.21" }, "dependencies": { "@supabase-cache-helpers/postgrest-core": "workspace:*", diff --git a/packages/postgrest-vue-query/src/mutate/types.ts b/packages/postgrest-vue-query/src/mutate/types.ts index 92e6778c..74b3aae1 100644 --- a/packages/postgrest-vue-query/src/mutate/types.ts +++ b/packages/postgrest-vue-query/src/mutate/types.ts @@ -11,7 +11,7 @@ import { UpsertFetcherOptions, RevalidateOpts, } from '@supabase-cache-helpers/postgrest-core'; -import { UseMutationOptions } from '@tanstack/vue-query'; +import type { UseMutationOptions } from '@tanstack/vue-query'; export type Operation = | 'Insert' diff --git a/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts index a7aac9df..f16d8bab 100644 --- a/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts +++ b/packages/postgrest-vue-query/src/mutate/use-delete-many-mutation.ts @@ -20,7 +20,7 @@ import { useQueriesForTableLoader } from '../lib'; * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table * @param {Array} primaryKeys Array of primary keys of the table * @param {string | null} query Optional PostgREST query string for the DELETE mutation - * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + * @param {UsePostgrestMutationOpts} [opts] Options to configure the hook */ function useDeleteManyMutation< S extends GenericSchema, @@ -33,10 +33,7 @@ function useDeleteManyMutation< qb: PostgrestQueryBuilder, primaryKeys: (keyof T['Row'])[], query?: Q | null, - opts?: Omit< - UsePostgrestMutationOpts, - 'mutationFn' - >, + opts?: UsePostgrestMutationOpts, ) { const queriesForTable = useQueriesForTableLoader(getTable(qb)); const deleteItem = useDeleteItem({ diff --git a/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts index f8bbf34b..4ed79219 100644 --- a/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts +++ b/packages/postgrest-vue-query/src/mutate/use-delete-mutation.ts @@ -20,7 +20,7 @@ import { useQueriesForTableLoader } from '../lib'; * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table * @param {Array} primaryKeys Array of primary keys of the table * @param {string | null} query Optional PostgREST query string for the DELETE mutation - * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + * @param {UsePostgrestMutationOpts} [opts] Options to configure the hook */ function useDeleteMutation< S extends GenericSchema, @@ -33,10 +33,7 @@ function useDeleteMutation< qb: PostgrestQueryBuilder, primaryKeys: (keyof T['Row'])[], query?: Q | null, - opts?: Omit< - UsePostgrestMutationOpts, - 'mutationFn' - >, + opts?: UsePostgrestMutationOpts, ) { const queriesForTable = useQueriesForTableLoader(getTable(qb)); const deleteItem = useDeleteItem({ diff --git a/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts index f70e9baa..cf710cb4 100644 --- a/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts +++ b/packages/postgrest-vue-query/src/mutate/use-insert-mutation.ts @@ -21,7 +21,7 @@ import { useQueriesForTableLoader } from '../lib'; * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table * @param {Array} primaryKeys Array of primary keys of the table * @param {string | null} query Optional PostgREST query string for the INSERT mutation - * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + * @param {UsePostgrestMutationOpts} [opts] Options to configure the hook */ function useInsertMutation< S extends GenericSchema, @@ -34,10 +34,7 @@ function useInsertMutation< qb: PostgrestQueryBuilder, primaryKeys: (keyof T['Row'])[], query?: Q | null, - opts?: Omit< - UsePostgrestMutationOpts, - 'mutationFn' - >, + opts?: UsePostgrestMutationOpts, ) { const queriesForTable = useQueriesForTableLoader(getTable(qb)); const upsertItem = useUpsertItem({ diff --git a/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts index cc3e7c84..ab05c104 100644 --- a/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts +++ b/packages/postgrest-vue-query/src/mutate/use-update-mutation.ts @@ -20,7 +20,7 @@ import { useQueriesForTableLoader } from '../lib'; * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table * @param {Array} primaryKeys Array of primary keys of the table * @param {string | null} query Optional PostgREST query string for the UPDATE mutation - * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + * @param {UsePostgrestMutationOpts} [opts] Options to configure the hook */ function useUpdateMutation< S extends GenericSchema, @@ -33,10 +33,7 @@ function useUpdateMutation< qb: PostgrestQueryBuilder, primaryKeys: (keyof T['Row'])[], query?: Q | null, - opts?: Omit< - UsePostgrestMutationOpts, - 'mutationFn' - >, + opts?: UsePostgrestMutationOpts, ) { const queriesForTable = useQueriesForTableLoader(getTable(qb)); const upsertItem = useUpsertItem({ diff --git a/packages/postgrest-vue-query/src/mutate/use-upsert-mutation.ts b/packages/postgrest-vue-query/src/mutate/use-upsert-mutation.ts index 718b87f5..62a9b60c 100644 --- a/packages/postgrest-vue-query/src/mutate/use-upsert-mutation.ts +++ b/packages/postgrest-vue-query/src/mutate/use-upsert-mutation.ts @@ -21,7 +21,7 @@ import { useQueriesForTableLoader } from '../lib'; * @param {PostgrestQueryBuilder} qb PostgrestQueryBuilder instance for the table * @param {Array} primaryKeys Array of primary keys of the table * @param {string | null} query Optional PostgREST query string for the UPSERT mutation - * @param {Omit, 'mutationFn'>} [opts] Options to configure the hook + * @param {UsePostgrestMutationOpts} [opts] Options to configure the hook */ function useUpsertMutation< S extends GenericSchema, @@ -34,10 +34,7 @@ function useUpsertMutation< qb: PostgrestQueryBuilder, primaryKeys: (keyof T['Row'])[], query?: Q | null, - opts?: Omit< - UsePostgrestMutationOpts, - 'mutationFn' - >, + opts?: UsePostgrestMutationOpts, ) { const queriesForTable = useQueriesForTableLoader(getTable(qb)); const upsertItem = useUpsertItem({ diff --git a/packages/postgrest-vue-query/src/query/use-query.ts b/packages/postgrest-vue-query/src/query/use-query.ts index adc8c85e..43ba9ff1 100644 --- a/packages/postgrest-vue-query/src/query/use-query.ts +++ b/packages/postgrest-vue-query/src/query/use-query.ts @@ -10,6 +10,7 @@ import { UseQueryReturnType as UseVueQueryResult, UseQueryOptions as UseVueQueryOptions, } from '@tanstack/vue-query'; +import { toRef, ref, unref } from 'vue'; import { buildQueryOpts } from './build-query-opts'; @@ -81,10 +82,7 @@ export type UseQueryAnyReturn = Omit< */ function useQuery( query: PromiseLike>, - config?: Omit< - UseVueQueryOptions, PostgrestError>, - 'queryKey' | 'queryFn' - >, + config?: UseVueQueryOptions, PostgrestError>, ): UseQuerySingleReturn; /** * Vue hook to execute a PostgREST query and return a maybe single item response. @@ -95,9 +93,9 @@ function useQuery( */ function useQuery( query: PromiseLike>, - config?: Omit< - UseVueQueryOptions, PostgrestError>, - 'queryKey' | 'queryFn' + config?: UseVueQueryOptions< + PostgrestMaybeSingleResponse, + PostgrestError >, ): UseQueryMaybeSingleReturn; /** @@ -110,10 +108,7 @@ function useQuery( */ function useQuery( query: PromiseLike>, - config?: Omit< - UseVueQueryOptions, PostgrestError>, - 'queryKey' | 'queryFn' - >, + config?: UseVueQueryOptions, PostgrestError>, ): UseQueryReturn; /** @@ -126,10 +121,7 @@ function useQuery( */ function useQuery( query: PromiseLike>, - config?: Omit< - UseVueQueryOptions, PostgrestError>, - 'queryKey' | 'queryFn' - >, + config?: UseVueQueryOptions, PostgrestError>, ): UseQueryAnyReturn { const { data, ...rest } = useVueQuery< AnyPostgrestResponse, diff --git a/packages/storage-vue-query/.eslintrc.json b/packages/storage-vue-query/.eslintrc.json index c54a0612..de95d41d 100644 --- a/packages/storage-vue-query/.eslintrc.json +++ b/packages/storage-vue-query/.eslintrc.json @@ -1,4 +1,12 @@ { "root": true, - "extends": ["@supabase-cache-helpers/custom"] + "parser": "vue-eslint-parser", + "parserOptions": { + "parser": "@typescript-eslint/parser" + }, + "extends": [ + "@supabase-cache-helpers/custom", + "plugin:vue/vue3-recommended", + "@vue/typescript/recommended" + ] } diff --git a/packages/storage-vue-query/__tests__/components/DirectoryPage.vue b/packages/storage-vue-query/__tests__/components/DirectoryPage.vue new file mode 100644 index 00000000..a20630ad --- /dev/null +++ b/packages/storage-vue-query/__tests__/components/DirectoryPage.vue @@ -0,0 +1,24 @@ + + + diff --git a/packages/storage-vue-query/__tests__/components/DirectoryUrlsPage.vue b/packages/storage-vue-query/__tests__/components/DirectoryUrlsPage.vue new file mode 100644 index 00000000..1e7d093f --- /dev/null +++ b/packages/storage-vue-query/__tests__/components/DirectoryUrlsPage.vue @@ -0,0 +1,24 @@ + + + diff --git a/packages/storage-vue-query/__tests__/components/FileUrlPage.vue b/packages/storage-vue-query/__tests__/components/FileUrlPage.vue new file mode 100644 index 00000000..8d5aa3af --- /dev/null +++ b/packages/storage-vue-query/__tests__/components/FileUrlPage.vue @@ -0,0 +1,24 @@ + + + diff --git a/packages/storage-vue-query/__tests__/components/RemoveDirectoryPage.vue b/packages/storage-vue-query/__tests__/components/RemoveDirectoryPage.vue new file mode 100644 index 00000000..608ae9f2 --- /dev/null +++ b/packages/storage-vue-query/__tests__/components/RemoveDirectoryPage.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/storage-vue-query/__tests__/components/RemoveFilesPage.vue b/packages/storage-vue-query/__tests__/components/RemoveFilesPage.vue new file mode 100644 index 00000000..6fc4bed0 --- /dev/null +++ b/packages/storage-vue-query/__tests__/components/RemoveFilesPage.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/storage-vue-query/__tests__/components/UploadPage.vue b/packages/storage-vue-query/__tests__/components/UploadPage.vue new file mode 100644 index 00000000..313392bc --- /dev/null +++ b/packages/storage-vue-query/__tests__/components/UploadPage.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.tsx b/packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.ts similarity index 65% rename from packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.tsx rename to packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.ts index a29fcd80..5780966b 100644 --- a/packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.tsx +++ b/packages/storage-vue-query/__tests__/mutate/use-remove-directory.spec.ts @@ -1,9 +1,10 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { fetchDirectory } from '@supabase-cache-helpers/storage-core'; -import { fireEvent, screen } from '@testing-library/react'; +import { fireEvent, screen } from '@testing-library/vue'; +import 'ts-jest/globals'; -import { useDirectory, useRemoveDirectory } from '../../src'; import { cleanup, renderWithConfig, upload } from '../utils'; +import Page from '../components/RemoveDirectoryPage.vue'; const TEST_PREFIX = 'postgrest-storage-remove'; @@ -28,22 +29,7 @@ describe('useRemoveDirectory', () => { }); it('should remove all files in a directory', async () => { - function Page() { - useDirectory(client.storage.from('private_contact_files'), dirName, { - refetchOnWindowFocus: false, - }); - const { mutateAsync: remove, isSuccess } = useRemoveDirectory( - client.storage.from('private_contact_files'), - ); - return ( - <> -
remove(dirName)} /> -
{`isSuccess: ${isSuccess}`}
- - ); - } - - renderWithConfig(); + renderWithConfig(Page, { client, dirName }); fireEvent.click(screen.getByTestId('remove')); await screen.findByText('isSuccess: true', {}, { timeout: 10000 }); await expect( diff --git a/packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.tsx b/packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.ts similarity index 65% rename from packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.tsx rename to packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.ts index a0582a86..e984a20f 100644 --- a/packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.tsx +++ b/packages/storage-vue-query/__tests__/mutate/use-remove-files.spec.ts @@ -1,9 +1,10 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { fetchDirectory } from '@supabase-cache-helpers/storage-core'; import { fireEvent, screen } from '@testing-library/react'; +import 'ts-jest/globals'; -import { useDirectory, useRemoveFiles } from '../../src'; import { cleanup, renderWithConfig, upload } from '../utils'; +import Page from '../components/RemoveFilesPage.vue'; const TEST_PREFIX = 'postgrest-storage-remove'; @@ -28,25 +29,7 @@ describe('useRemoveFiles', () => { }); it('should remove files', async () => { - function Page() { - useDirectory(client.storage.from('private_contact_files'), dirName, { - refetchOnWindowFocus: false, - }); - const { mutateAsync: remove, isSuccess } = useRemoveFiles( - client.storage.from('private_contact_files'), - ); - return ( - <> -
remove(files.map((f) => [dirName, f].join('/')))} - /> -
{`isSuccess: ${isSuccess}`}
- - ); - } - - renderWithConfig(); + renderWithConfig(Page, { client, dirName, files }); fireEvent.click(screen.getByTestId('remove')); await screen.findByText('isSuccess: true', {}, { timeout: 10000 }); await expect( diff --git a/packages/storage-vue-query/__tests__/mutate/use-upload.spec.tsx b/packages/storage-vue-query/__tests__/mutate/use-upload.spec.ts similarity index 69% rename from packages/storage-vue-query/__tests__/mutate/use-upload.spec.tsx rename to packages/storage-vue-query/__tests__/mutate/use-upload.spec.ts index 7ff58f24..e5fce876 100644 --- a/packages/storage-vue-query/__tests__/mutate/use-upload.spec.tsx +++ b/packages/storage-vue-query/__tests__/mutate/use-upload.spec.ts @@ -1,9 +1,10 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { fetchDirectory } from '@supabase-cache-helpers/storage-core'; import { fireEvent, screen } from '@testing-library/react'; +import 'ts-jest/globals'; -import { useDirectory, useUpload } from '../../src'; import { cleanup, loadFixtures, renderWithConfig } from '../utils'; +import Page from '../components/UploadPage.vue'; const TEST_PREFIX = 'postgrest-storage-upload'; @@ -31,26 +32,7 @@ describe('useUpload', () => { }); it('should upload files', async () => { - function Page() { - useDirectory(client.storage.from('private_contact_files'), dirName, { - refetchOnWindowFocus: false, - }); - const { mutateAsync: upload, isSuccess } = useUpload( - client.storage.from('private_contact_files'), - {}, - ); - return ( - <> -
upload({ files, path: dirName })} - /> -
{`isSuccess: ${isSuccess}`}
- - ); - } - - renderWithConfig(); + renderWithConfig(Page, { client, dirName, files }); fireEvent.click(screen.getByTestId('upload')); await screen.findByText('isSuccess: true', {}, { timeout: 10000 }); await expect( diff --git a/packages/storage-vue-query/__tests__/query/use-directory-urls.spec.tsx b/packages/storage-vue-query/__tests__/query/use-directory-urls.spec.ts similarity index 68% rename from packages/storage-vue-query/__tests__/query/use-directory-urls.spec.tsx rename to packages/storage-vue-query/__tests__/query/use-directory-urls.spec.ts index 97b30acc..11f9c2b4 100644 --- a/packages/storage-vue-query/__tests__/query/use-directory-urls.spec.tsx +++ b/packages/storage-vue-query/__tests__/query/use-directory-urls.spec.ts @@ -1,8 +1,9 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { screen } from '@testing-library/react'; +import 'ts-jest/globals'; -import { useDirectoryFileUrls } from '../../src'; import { cleanup, renderWithConfig, upload } from '../utils'; +import Page from '../components/DirectoryUrlsPage.vue'; const TEST_PREFIX = 'postgrest-storage-directory-urls'; @@ -29,25 +30,7 @@ describe('useDirectoryFileUrls', () => { }); it('should return files', async () => { - function Page() { - const { data: files } = useDirectoryFileUrls( - client.storage.from('private_contact_files'), - dirName, - 'private', - { - refetchOnWindowFocus: false, - }, - ); - return ( -
- {(files ?? []).map((f) => ( - {`${f.name}: ${f.url ? 'exists' : f.url}`} - ))} -
- ); - } - - renderWithConfig(); + renderWithConfig(Page, { client, dirName }); await Promise.all( privateFiles.map( async (f) => diff --git a/packages/storage-vue-query/__tests__/query/use-directory.spec.tsx b/packages/storage-vue-query/__tests__/query/use-directory.spec.ts similarity index 71% rename from packages/storage-vue-query/__tests__/query/use-directory.spec.tsx rename to packages/storage-vue-query/__tests__/query/use-directory.spec.ts index 224cde66..8f081a45 100644 --- a/packages/storage-vue-query/__tests__/query/use-directory.spec.tsx +++ b/packages/storage-vue-query/__tests__/query/use-directory.spec.ts @@ -1,8 +1,9 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { screen } from '@testing-library/react'; +import 'ts-jest/globals'; -import { useDirectory } from '../../src'; import { cleanup, renderWithConfig, upload } from '../utils'; +import Page from '../components/DirectoryPage.vue'; const TEST_PREFIX = 'postgrest-storage-directory'; @@ -29,24 +30,7 @@ describe('useDirectory', () => { }); it('should return files', async () => { - function Page() { - const { data: files } = useDirectory( - client.storage.from('private_contact_files'), - dirName, - { - refetchOnWindowFocus: false, - }, - ); - return ( -
- {(files ?? []).map((f) => ( - {f.name} - ))} -
- ); - } - - renderWithConfig(); + renderWithConfig(Page, { client, dirName }); await Promise.all( privateFiles.map( async (f) => await screen.findByText(f, {}, { timeout: 10000 }), diff --git a/packages/storage-vue-query/__tests__/query/use-file-url.spec.tsx b/packages/storage-vue-query/__tests__/query/use-file-url.spec.ts similarity index 71% rename from packages/storage-vue-query/__tests__/query/use-file-url.spec.tsx rename to packages/storage-vue-query/__tests__/query/use-file-url.spec.ts index 71d83b2c..30bc06c5 100644 --- a/packages/storage-vue-query/__tests__/query/use-file-url.spec.tsx +++ b/packages/storage-vue-query/__tests__/query/use-file-url.spec.ts @@ -1,8 +1,9 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { screen } from '@testing-library/react'; +import 'ts-jest/globals'; -import { useFileUrl } from '../../src'; import { cleanup, renderWithConfig, upload } from '../utils'; +import Page from '../components/FileUrlPage.vue'; const TEST_PREFIX = 'postgrest-storage-file-url'; @@ -29,20 +30,7 @@ describe('useFileUrl', () => { }); it('should return file url', async () => { - function Page() { - const { data: url } = useFileUrl( - client.storage.from('public_contact_files'), - `${dirName}/${publicFiles[0]}`, - 'public', - { - ensureExistence: true, - refetchOnWindowFocus: false, - }, - ); - return
{`URL: ${url ? 'exists' : url}`}
; - } - - renderWithConfig(); + renderWithConfig(Page, { client, dirName, publicFiles }); await screen.findByText('URL: exists', {}, { timeout: 10000 }); }); }); diff --git a/packages/storage-vue-query/__tests__/utils.tsx b/packages/storage-vue-query/__tests__/utils.ts similarity index 78% rename from packages/storage-vue-query/__tests__/utils.tsx rename to packages/storage-vue-query/__tests__/utils.ts index b0bda79d..21201acc 100644 --- a/packages/storage-vue-query/__tests__/utils.tsx +++ b/packages/storage-vue-query/__tests__/utils.ts @@ -1,24 +1,24 @@ import { SupabaseClient } from '@supabase/supabase-js'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render } from '@testing-library/react'; +import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'; +import { render } from '@testing-library/vue'; import * as dotenv from 'dotenv'; import { readdir, readFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; -import React from 'react'; dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); export const renderWithConfig = ( - element: React.ReactElement, + element: any, + props?: { [key: string]: unknown }, queryClient?: QueryClient, ): ReturnType => { const client = queryClient ?? new QueryClient(); - const TestQueryClientProvider = ({ - children, - }: { - children: React.ReactNode; - }) => {children} ; - return render(element, { wrapper: TestQueryClientProvider }); + return render(element, { + props, + global: { + plugins: [[VueQueryPlugin, { queryClient: client }]], + }, + }); }; export const loadFixtures = async () => { diff --git a/packages/storage-vue-query/package.json b/packages/storage-vue-query/package.json index c2293525..b5c7167e 100644 --- a/packages/storage-vue-query/package.json +++ b/packages/storage-vue-query/package.json @@ -48,31 +48,32 @@ "directory": "packages/storage-vue-query" }, "peerDependencies": { + "@supabase/storage-js": "^2.4.0", "@tanstack/vue-query": "^5.28.13", - "vue": "^3.4.21", - "@supabase/storage-js": "^2.4.0" + "vue": "^3.4.21" }, "jest": { "preset": "@supabase-cache-helpers/jest-presets/jest/node" }, "devDependencies": { - "@supabase/supabase-js": "2.38.5", + "@supabase-cache-helpers/eslint-config-custom": "workspace:*", + "@supabase-cache-helpers/jest-presets": "workspace:*", + "@supabase-cache-helpers/prettier-config": "workspace:*", + "@supabase-cache-helpers/tsconfig": "workspace:*", "@supabase/storage-js": "2.5.5", - "@testing-library/vue": "8.0.3", + "@supabase/supabase-js": "2.38.5", "@testing-library/jest-dom": "6.4.0", - "jest-environment-jsdom": "29.7.0", + "@testing-library/vue": "8.0.3", "@types/jest": "29.5.0", "dotenv": "16.4.0", "eslint": "8.54.0", - "@supabase-cache-helpers/eslint-config-custom": "workspace:*", + "eslint-plugin-vue": "^9.25.0", "jest": "29.7.0", - "@supabase-cache-helpers/jest-presets": "workspace:*", - "@supabase-cache-helpers/prettier-config": "workspace:*", + "jest-environment-jsdom": "29.7.0", "ts-jest": "29.1.0", - "@supabase-cache-helpers/tsconfig": "workspace:*", "tsup": "8.0.0", - "vue": "3.4.21", - "typescript": "5.4.2" + "typescript": "5.4.2", + "vue": "3.4.21" }, "dependencies": { "@supabase-cache-helpers/storage-core": "workspace:*" diff --git a/packages/storage-vue-query/src/mutate/use-upload.ts b/packages/storage-vue-query/src/mutate/use-upload.ts index 40c77eae..6f54208d 100644 --- a/packages/storage-vue-query/src/mutate/use-upload.ts +++ b/packages/storage-vue-query/src/mutate/use-upload.ts @@ -18,7 +18,6 @@ import { decode, getBucketId, StorageFileApi, truthy } from '../lib'; export type { UploadFetcherConfig, UploadFileResponse, FileInput }; export type UseUploadInput = { - file: FileObject; files: FileList | (File | FileInput)[]; path?: string; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b190daa8..0d57af37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -763,6 +763,9 @@ importers: eslint: specifier: 8.54.0 version: 8.54.0 + eslint-plugin-vue: + specifier: ^9.25.0 + version: 9.25.0(eslint@8.54.0) jest: specifier: 29.7.0 version: 29.7.0 @@ -771,7 +774,7 @@ importers: version: 29.7.0 ts-jest: specifier: 29.1.0 - version: 29.1.0(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2) + version: 29.1.0(@babel/core@7.23.5)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2) tsup: specifier: 8.0.0 version: 8.0.0(typescript@5.4.2) @@ -1006,6 +1009,9 @@ importers: eslint: specifier: 8.54.0 version: 8.54.0 + eslint-plugin-vue: + specifier: ^9.25.0 + version: 9.25.0(eslint@8.54.0) jest: specifier: 29.7.0 version: 29.7.0 @@ -2218,7 +2224,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 17.0.45 + '@types/node': 20.12.7 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -2238,14 +2244,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.1 + '@types/node': 20.12.7 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.10.1) + jest-config: 29.7.0(@types/node@20.12.7) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -2272,7 +2278,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.1 + '@types/node': 20.12.7 jest-mock: 29.7.0 /@jest/expect-utils@29.7.0: @@ -2296,7 +2302,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.10.1 + '@types/node': 20.12.7 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2327,7 +2333,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.10.1 + '@types/node': 20.12.7 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -2409,7 +2415,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.10.1 + '@types/node': 20.12.7 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -4560,7 +4566,7 @@ packages: /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 20.10.1 + '@types/node': 20.12.7 /@types/hast@2.3.4: resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==} @@ -4604,7 +4610,7 @@ packages: /@types/jsdom@20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 20.10.1 + '@types/node': 20.12.7 '@types/tough-cookie': 4.0.5 parse5: 7.1.2 dev: true @@ -4658,11 +4664,18 @@ packages: /@types/node@17.0.45: resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + dev: true /@types/node@20.10.1: resolution: {integrity: sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==} dependencies: undici-types: 5.26.5 + dev: true + + /@types/node@20.12.7: + resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + dependencies: + undici-types: 5.26.5 /@types/node@20.9.2: resolution: {integrity: sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==} @@ -4733,7 +4746,7 @@ packages: /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: - '@types/node': 20.10.1 + '@types/node': 20.12.7 /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -5448,6 +5461,10 @@ packages: readable-stream: 3.6.1 dev: false + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -5883,7 +5900,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.10.1) + jest-config: 29.7.0(@types/node@20.12.7) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -6522,7 +6539,7 @@ packages: '@one-ini/wasm': 0.1.1 commander: 10.0.1 minimatch: 9.0.1 - semver: 7.5.4 + semver: 7.6.0 dev: true /electron-to-chromium@1.4.325: @@ -7152,6 +7169,25 @@ packages: eslint: 8.54.0 dev: false + /eslint-plugin-vue@9.25.0(eslint@8.54.0): + resolution: {integrity: sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0) + eslint: 8.54.0 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.0.16 + semver: 7.6.0 + vue-eslint-parser: 9.4.2(eslint@8.54.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7695,6 +7731,13 @@ packages: dependencies: type-fest: 0.20.2 + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + /globalthis@1.0.3: resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} @@ -8376,7 +8419,7 @@ packages: '@babel/parser': 7.24.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color @@ -8438,7 +8481,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 17.0.45 + '@types/node': 20.12.7 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -8475,7 +8518,7 @@ packages: create-jest: 29.7.0 exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.10.1) + jest-config: 29.7.0(@types/node@20.12.7) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.1 @@ -8485,7 +8528,7 @@ packages: - supports-color - ts-node - /jest-config@29.7.0(@types/node@20.10.1): + /jest-config@29.7.0(@types/node@20.12.7): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -8500,7 +8543,7 @@ packages: '@babel/core': 7.24.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.1 + '@types/node': 20.12.7 babel-jest: 29.7.0(@babel/core@7.24.0) chalk: 4.1.2 ci-info: 3.9.0 @@ -8579,7 +8622,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 17.0.45 + '@types/node': 20.12.7 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -8593,7 +8636,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.10.1 + '@types/node': 20.12.7 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -8640,7 +8683,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.1 + '@types/node': 20.12.7 jest-util: 29.7.0 /jest-pnp-resolver@1.2.2(jest-resolve@29.7.0): @@ -8690,7 +8733,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.1 + '@types/node': 20.12.7 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -8720,7 +8763,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 17.0.45 + '@types/node': 20.12.7 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -8770,7 +8813,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.1 + '@types/node': 20.12.7 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -8793,7 +8836,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 17.0.45 + '@types/node': 20.12.7 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -8804,7 +8847,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 17.0.45 + '@types/node': 20.12.7 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -10246,6 +10289,12 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + /nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} dev: true @@ -10586,6 +10635,14 @@ packages: cssesc: 3.0.0 util-deprecate: 1.0.2 + /postcss-selector-parser@6.0.16: + resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + /postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -11299,6 +11356,13 @@ packages: dependencies: lru-cache: 6.0.0 + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + /server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} dev: false @@ -11996,6 +12060,41 @@ packages: /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + /ts-jest@29.1.0(@babel/core@7.23.5)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2): + resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.23.5 + bs-logger: 0.2.6 + esbuild: 0.19.8 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0 + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 5.4.2 + yargs-parser: 21.1.1 + dev: true + /ts-jest@29.1.0(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2): resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -12655,6 +12754,24 @@ packages: vue: 3.4.21(typescript@5.4.2) dev: false + /vue-eslint-parser@9.4.2(eslint@8.54.0): + resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + debug: 4.3.4 + eslint: 8.54.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + lodash: 4.17.21 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + dev: true + /vue@3.4.21(typescript@5.4.2): resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==} peerDependencies: From b5fe26b85baf10213f49d9addf9a3f82204dbfe6 Mon Sep 17 00:00:00 2001 From: Christian Pannwitz Date: Sat, 25 May 2024 22:13:37 +0200 Subject: [PATCH 5/5] update dependencies --- packages/postgrest-vue-query/package.json | 30 +- packages/storage-vue-query/package.json | 26 +- pnpm-lock.yaml | 649 +++++++++++++++------- 3 files changed, 477 insertions(+), 228 deletions(-) diff --git a/packages/postgrest-vue-query/package.json b/packages/postgrest-vue-query/package.json index 6a0fc33a..06999a14 100644 --- a/packages/postgrest-vue-query/package.json +++ b/packages/postgrest-vue-query/package.json @@ -48,9 +48,9 @@ "directory": "packages/postgrest-vue-query" }, "peerDependencies": { - "@supabase/postgrest-js": "^1.9.0", - "@tanstack/vue-query": "^5.28.13", - "vue": "^3.4.21" + "@supabase/postgrest-js": "^1.15.4", + "@tanstack/vue-query": "^5.38.0", + "vue": "^3.4.27" }, "jest": { "preset": "@supabase-cache-helpers/jest-presets/jest/node" @@ -60,21 +60,21 @@ "@supabase-cache-helpers/jest-presets": "workspace:*", "@supabase-cache-helpers/prettier-config": "workspace:*", "@supabase-cache-helpers/tsconfig": "workspace:*", - "@supabase/postgrest-js": "1.9.0", - "@supabase/supabase-js": "2.38.5", - "@testing-library/jest-dom": "6.4.0", - "@testing-library/vue": "8.0.3", - "@types/flat": "5.0.2", - "@types/jest": "29.5.0", - "dotenv": "16.4.0", + "@supabase/postgrest-js": "1.15.4", + "@supabase/supabase-js": "2.43.4", + "@testing-library/jest-dom": "6.4.5", + "@testing-library/vue": "8.1.0", + "@types/flat": "5.0.5", + "@types/jest": "29.5.12", + "dotenv": "16.4.5", "eslint": "8.54.0", - "eslint-plugin-vue": "^9.25.0", + "eslint-plugin-vue": "^9.26.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "ts-jest": "29.1.0", - "tsup": "8.0.0", - "typescript": "5.4.2", - "vue": "3.4.21" + "ts-jest": "29.1.3", + "tsup": "8.0.2", + "typescript": "5.4.5", + "vue": "3.4.27" }, "dependencies": { "@supabase-cache-helpers/postgrest-core": "workspace:*", diff --git a/packages/storage-vue-query/package.json b/packages/storage-vue-query/package.json index b5c7167e..4e1bfb8e 100644 --- a/packages/storage-vue-query/package.json +++ b/packages/storage-vue-query/package.json @@ -48,9 +48,9 @@ "directory": "packages/storage-vue-query" }, "peerDependencies": { - "@supabase/storage-js": "^2.4.0", - "@tanstack/vue-query": "^5.28.13", - "vue": "^3.4.21" + "@supabase/storage-js": "^2.5.5", + "@tanstack/vue-query": "^5.38.0", + "vue": "^3.4.27" }, "jest": { "preset": "@supabase-cache-helpers/jest-presets/jest/node" @@ -61,19 +61,19 @@ "@supabase-cache-helpers/prettier-config": "workspace:*", "@supabase-cache-helpers/tsconfig": "workspace:*", "@supabase/storage-js": "2.5.5", - "@supabase/supabase-js": "2.38.5", - "@testing-library/jest-dom": "6.4.0", - "@testing-library/vue": "8.0.3", - "@types/jest": "29.5.0", - "dotenv": "16.4.0", + "@supabase/supabase-js": "2.43.4", + "@testing-library/jest-dom": "6.4.5", + "@testing-library/vue": "8.1.0", + "@types/jest": "29.5.12", + "dotenv": "16.4.5", "eslint": "8.54.0", - "eslint-plugin-vue": "^9.25.0", + "eslint-plugin-vue": "^9.26.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "ts-jest": "29.1.0", - "tsup": "8.0.0", - "typescript": "5.4.2", - "vue": "3.4.21" + "ts-jest": "29.1.3", + "tsup": "8.0.2", + "typescript": "5.4.5", + "vue": "3.4.27" }, "dependencies": { "@supabase-cache-helpers/storage-core": "workspace:*" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d57af37..a2fa519b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,22 +37,22 @@ importers: version: 18.2.19 '@vercel/analytics': specifier: ^1.0.0 - version: 1.2.2(next@14.1.0)(react@18.2.0) + version: 1.2.2(next@14.2.0)(react@18.2.0) eslint: specifier: 8.54.0 version: 8.54.0 eslint-config-next: - specifier: 14.1.0 - version: 14.1.0(eslint@8.54.0)(typescript@5.4.2) + specifier: 14.2.0 + version: 14.2.0(eslint@8.54.0)(typescript@5.4.2) next: - specifier: 14.1.0 - version: 14.1.0(react-dom@18.2.0)(react@18.2.0) + specifier: 14.2.0 + version: 14.2.0(react-dom@18.2.0)(react@18.2.0) nextra: specifier: 2.13.2 - version: 2.13.2(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) + version: 2.13.2(next@14.2.0)(react-dom@18.2.0)(react@18.2.0) nextra-theme-docs: specifier: 2.13.2 - version: 2.13.2(next@14.1.0)(nextra@2.13.2)(react-dom@18.2.0)(react@18.2.0) + version: 2.13.2(next@14.2.0)(nextra@2.13.2)(react-dom@18.2.0)(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -492,8 +492,8 @@ importers: specifier: 1.10.16 version: 1.10.16(eslint@8.54.0) eslint-config-universe: - specifier: 12.0.0 - version: 12.0.0(eslint@8.54.0)(prettier@3.2.0)(typescript@5.4.2) + specifier: 12.1.0 + version: 12.1.0(eslint@8.54.0)(prettier@3.2.0)(typescript@5.4.2) devDependencies: typescript: specifier: 5.4.2 @@ -503,7 +503,7 @@ importers: dependencies: ts-jest: specifier: 29.1.0 - version: 29.1.0(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2) + version: 29.1.0(jest@29.7.0)(typescript@5.4.5) packages/postgrest-core: dependencies: @@ -558,7 +558,7 @@ importers: version: 29.7.0 ts-jest: specifier: 29.1.0 - version: 29.1.0(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2) + version: 29.1.0(@babel/core@7.23.5)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2) tsup: specifier: 8.0.0 version: 8.0.0(typescript@5.4.2) @@ -600,8 +600,8 @@ importers: specifier: 6.4.0 version: 6.4.0(@types/jest@29.5.0)(jest@29.7.0) '@testing-library/react': - specifier: 14.2.0 - version: 14.2.0(react-dom@18.2.0)(react@18.2.0) + specifier: 14.3.0 + version: 14.3.0(react-dom@18.2.0)(react@18.2.0) '@types/flat': specifier: 5.0.2 version: 5.0.2 @@ -676,8 +676,8 @@ importers: specifier: 6.4.0 version: 6.4.0(@types/jest@29.5.0)(jest@29.7.0) '@testing-library/react': - specifier: 14.2.0 - version: 14.2.0(react-dom@18.2.0)(react@18.2.0) + specifier: 14.3.0 + version: 14.3.0(react-dom@18.2.0)(react@18.2.0) '@types/flat': specifier: 5.0.2 version: 5.0.2 @@ -721,8 +721,8 @@ importers: specifier: workspace:* version: link:../postgrest-core '@tanstack/vue-query': - specifier: ^5.28.13 - version: 5.28.13(vue@3.4.21) + specifier: ^5.38.0 + version: 5.38.0(vue@3.4.27) flat: specifier: 5.0.2 version: 5.0.2 @@ -740,32 +740,32 @@ importers: specifier: workspace:* version: link:../tsconfig '@supabase/postgrest-js': - specifier: 1.9.0 - version: 1.9.0 + specifier: 1.15.4 + version: 1.15.4 '@supabase/supabase-js': - specifier: 2.38.5 - version: 2.38.5 + specifier: 2.43.4 + version: 2.43.4 '@testing-library/jest-dom': - specifier: 6.4.0 - version: 6.4.0(@types/jest@29.5.0)(jest@29.7.0) + specifier: 6.4.5 + version: 6.4.5(@types/jest@29.5.12)(jest@29.7.0) '@testing-library/vue': - specifier: 8.0.3 - version: 8.0.3(vue@3.4.21) + specifier: 8.1.0 + version: 8.1.0(vue@3.4.27) '@types/flat': - specifier: 5.0.2 - version: 5.0.2 + specifier: 5.0.5 + version: 5.0.5 '@types/jest': - specifier: 29.5.0 - version: 29.5.0 + specifier: 29.5.12 + version: 29.5.12 dotenv: - specifier: 16.4.0 - version: 16.4.0 + specifier: 16.4.5 + version: 16.4.5 eslint: specifier: 8.54.0 version: 8.54.0 eslint-plugin-vue: - specifier: ^9.25.0 - version: 9.25.0(eslint@8.54.0) + specifier: ^9.26.0 + version: 9.26.0(eslint@8.54.0) jest: specifier: 29.7.0 version: 29.7.0 @@ -773,17 +773,17 @@ importers: specifier: 29.7.0 version: 29.7.0 ts-jest: - specifier: 29.1.0 - version: 29.1.0(@babel/core@7.23.5)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2) + specifier: 29.1.3 + version: 29.1.3(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.5) tsup: - specifier: 8.0.0 - version: 8.0.0(typescript@5.4.2) + specifier: 8.0.2 + version: 8.0.2(typescript@5.4.5) typescript: - specifier: 5.4.2 - version: 5.4.2 + specifier: 5.4.5 + version: 5.4.5 vue: - specifier: 3.4.21 - version: 3.4.21(typescript@5.4.2) + specifier: 3.4.27 + version: 3.4.27(typescript@5.4.5) packages/prettier-config: devDependencies: @@ -864,8 +864,8 @@ importers: specifier: 6.4.0 version: 6.4.0(@types/jest@29.5.0)(jest@29.7.0) '@testing-library/react': - specifier: 14.2.0 - version: 14.2.0(react-dom@18.2.0)(react@18.2.0) + specifier: 14.3.0 + version: 14.3.0(react-dom@18.2.0)(react@18.2.0) '@types/jest': specifier: 29.5.0 version: 29.5.0 @@ -931,8 +931,8 @@ importers: specifier: 6.4.0 version: 6.4.0(@types/jest@29.5.0)(jest@29.7.0) '@testing-library/react': - specifier: 14.2.0 - version: 14.2.0(react-dom@18.2.0)(react@18.2.0) + specifier: 14.3.0 + version: 14.3.0(react-dom@18.2.0)(react@18.2.0) '@types/jest': specifier: 29.5.0 version: 29.5.0 @@ -973,8 +973,8 @@ importers: specifier: workspace:* version: link:../storage-core '@tanstack/vue-query': - specifier: ^5.28.13 - version: 5.28.13(vue@3.4.21) + specifier: ^5.38.0 + version: 5.38.0(vue@3.4.27) devDependencies: '@supabase-cache-helpers/eslint-config-custom': specifier: workspace:* @@ -992,26 +992,26 @@ importers: specifier: 2.5.5 version: 2.5.5 '@supabase/supabase-js': - specifier: 2.38.5 - version: 2.38.5 + specifier: 2.43.4 + version: 2.43.4 '@testing-library/jest-dom': - specifier: 6.4.0 - version: 6.4.0(@types/jest@29.5.0)(jest@29.7.0) + specifier: 6.4.5 + version: 6.4.5(@types/jest@29.5.12)(jest@29.7.0) '@testing-library/vue': - specifier: 8.0.3 - version: 8.0.3(vue@3.4.21) + specifier: 8.1.0 + version: 8.1.0(vue@3.4.27) '@types/jest': - specifier: 29.5.0 - version: 29.5.0 + specifier: 29.5.12 + version: 29.5.12 dotenv: - specifier: 16.4.0 - version: 16.4.0 + specifier: 16.4.5 + version: 16.4.5 eslint: specifier: 8.54.0 version: 8.54.0 eslint-plugin-vue: - specifier: ^9.25.0 - version: 9.25.0(eslint@8.54.0) + specifier: ^9.26.0 + version: 9.26.0(eslint@8.54.0) jest: specifier: 29.7.0 version: 29.7.0 @@ -1019,17 +1019,17 @@ importers: specifier: 29.7.0 version: 29.7.0 ts-jest: - specifier: 29.1.0 - version: 29.1.0(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.2) + specifier: 29.1.3 + version: 29.1.3(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.5) tsup: - specifier: 8.0.0 - version: 8.0.0(typescript@5.4.2) + specifier: 8.0.2 + version: 8.0.2(typescript@5.4.5) typescript: - specifier: 5.4.2 - version: 5.4.2 + specifier: 5.4.5 + version: 5.4.5 vue: - specifier: 3.4.21 - version: 3.4.21(typescript@5.4.2) + specifier: 3.4.27 + version: 3.4.27(typescript@5.4.5) packages/tsconfig: {} @@ -1364,6 +1364,13 @@ packages: dependencies: '@babel/types': 7.24.0 + /@babel/parser@7.24.6: + resolution: {integrity: sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.0 + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.5): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -1891,6 +1898,7 @@ packages: cpu: [arm64] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-arm@0.19.8: @@ -1899,6 +1907,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/android-x64@0.19.8: @@ -1907,6 +1916,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: true optional: true /@esbuild/darwin-arm64@0.19.8: @@ -1915,6 +1925,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: true optional: true /@esbuild/darwin-x64@0.19.8: @@ -1923,6 +1934,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: true optional: true /@esbuild/freebsd-arm64@0.19.8: @@ -1931,6 +1943,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: true optional: true /@esbuild/freebsd-x64@0.19.8: @@ -1939,6 +1952,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: true optional: true /@esbuild/linux-arm64@0.19.8: @@ -1947,6 +1961,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-arm@0.19.8: @@ -1955,6 +1970,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-ia32@0.19.8: @@ -1963,6 +1979,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-loong64@0.19.8: @@ -1971,6 +1988,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-mips64el@0.19.8: @@ -1979,6 +1997,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-ppc64@0.19.8: @@ -1987,6 +2006,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-riscv64@0.19.8: @@ -1995,6 +2015,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-s390x@0.19.8: @@ -2003,6 +2024,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/linux-x64@0.19.8: @@ -2011,6 +2033,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: true optional: true /@esbuild/netbsd-x64@0.19.8: @@ -2019,6 +2042,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: true optional: true /@esbuild/openbsd-x64@0.19.8: @@ -2027,6 +2051,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: true optional: true /@esbuild/sunos-x64@0.19.8: @@ -2035,6 +2060,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: true optional: true /@esbuild/win32-arm64@0.19.8: @@ -2043,6 +2069,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-ia32@0.19.8: @@ -2051,6 +2078,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: true optional: true /@esbuild/win32-x64@0.19.8: @@ -2059,6 +2087,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: true optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.54.0): @@ -2074,11 +2103,6 @@ packages: resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - /@eslint-community/regexpp@4.9.1: - resolution: {integrity: sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: false - /@eslint/eslintrc@2.1.3: resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2668,8 +2692,8 @@ packages: resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==} dev: false - /@next/env@14.1.0: - resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==} + /@next/env@14.2.0: + resolution: {integrity: sha512-4+70ELtSbRtYUuyRpAJmKC8NHBW2x1HMje9KO2Xd7IkoyucmV9SjgO+qeWMC0JWkRQXgydv1O7yKOK8nu/rITQ==} dev: false /@next/eslint-plugin-next@13.5.6: @@ -2678,8 +2702,8 @@ packages: glob: 7.1.7 dev: true - /@next/eslint-plugin-next@14.1.0: - resolution: {integrity: sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q==} + /@next/eslint-plugin-next@14.2.0: + resolution: {integrity: sha512-QkM01VPhwcupezVevy9Uyl1rmpg2PimhMjkb+ySmnPgSKUUM/PGGRQxdFgMpHv/JzQoC8kRySgKeM441GiizcA==} dependencies: glob: 10.3.10 dev: false @@ -2697,8 +2721,8 @@ packages: dev: false optional: true - /@next/swc-darwin-arm64@14.1.0: - resolution: {integrity: sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==} + /@next/swc-darwin-arm64@14.2.0: + resolution: {integrity: sha512-kHktLlw0AceuDnkVljJ/4lTJagLzDiO3klR1Fzl2APDFZ8r+aTxNaNcPmpp0xLMkgRwwk6sggYeqq0Rz9K4zzA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -2715,8 +2739,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.1.0: - resolution: {integrity: sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==} + /@next/swc-darwin-x64@14.2.0: + resolution: {integrity: sha512-HFSDu7lb1U3RDxXNeKH3NGRR5KyTPBSUTuIOr9jXoAso7i76gNYvnTjbuzGVWt2X5izpH908gmOYWtI7un+JrA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -2733,8 +2757,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.1.0: - resolution: {integrity: sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==} + /@next/swc-linux-arm64-gnu@14.2.0: + resolution: {integrity: sha512-iQsoWziO5ZMxDWZ4ZTCAc7hbJ1C9UDj/gATSqTaMjW2bJFwAsvf9UM79AKnljBl73uPZ+V0kH4rvnHTco4Ps2w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -2751,8 +2775,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@14.1.0: - resolution: {integrity: sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==} + /@next/swc-linux-arm64-musl@14.2.0: + resolution: {integrity: sha512-0JOk2uzLUt8fJK5LpsKKZa74zAch7bJjjgJzR9aOMs231AlE4gPYzsSm430ckZitjPGKeH5bgDZjqwqJQKIS2w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -2769,8 +2793,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@14.1.0: - resolution: {integrity: sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==} + /@next/swc-linux-x64-gnu@14.2.0: + resolution: {integrity: sha512-uYHkuTzX0NM6biKNp7hdKTf+BF0iMV254SxO0B8PgrQkxUBKGmk5ysHKB+FYBfdf9xei/t8OIKlXJs9ckD943A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -2787,8 +2811,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@14.1.0: - resolution: {integrity: sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==} + /@next/swc-linux-x64-musl@14.2.0: + resolution: {integrity: sha512-paN89nLs2dTBDtfXWty1/NVPit+q6ldwdktixYSVwiiAz647QDCd+EIYqoiS+/rPG3oXs/A7rWcJK9HVqfnMVg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -2805,8 +2829,8 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@14.1.0: - resolution: {integrity: sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==} + /@next/swc-win32-arm64-msvc@14.2.0: + resolution: {integrity: sha512-j1oiidZisnymYjawFqEfeGNcE22ZQ7lGUaa4pGOCVWrWeIDkPSj8zYgS9TzMNlg17Q3wSWCQC/F5uJAhSh7qcA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -2823,8 +2847,8 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.1.0: - resolution: {integrity: sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==} + /@next/swc-win32-ia32-msvc@14.2.0: + resolution: {integrity: sha512-6ff6F4xb+QGD1jhx/dOT9Ot7PQ/GAYekV9ykwEh2EFS/cLTyU4Y3cXkX5cNtNIhpctS5NvyjW9gIksRNErYE0A==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -2841,8 +2865,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.1.0: - resolution: {integrity: sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==} + /@next/swc-win32-x64-msvc@14.2.0: + resolution: {integrity: sha512-09DbG5vXAxz0eTFSf1uebWD36GF3D5toynRkgo2AlSrxwGZkWtJ1RhmrczRYQ17eD5bdo4FZ0ibiffdq5kc4vg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -4283,11 +4307,23 @@ packages: jose: 4.15.4 dev: false + /@supabase/auth-js@2.64.2: + resolution: {integrity: sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw==} + dependencies: + '@supabase/node-fetch': 2.6.15 + dev: true + /@supabase/functions-js@2.1.5: resolution: {integrity: sha512-BNzC5XhCzzCaggJ8s53DP+WeHHGT/NfTsx2wUSSGKR2/ikLFQTBCDzMvGz/PxYMqRko/LwncQtKXGOYp1PkPaw==} dependencies: '@supabase/node-fetch': 2.6.15 + /@supabase/functions-js@2.3.1: + resolution: {integrity: sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==} + dependencies: + '@supabase/node-fetch': 2.6.15 + dev: true + /@supabase/gotrue-js@2.62.2: resolution: {integrity: sha512-AP6e6W9rQXFTEJ7sTTNYQrNf0LCcnt1hUW+RIgUK+Uh3jbWvcIST7wAlYyNZiMlS9+PYyymWQ+Ykz/rOYSO0+A==} dependencies: @@ -4305,6 +4341,18 @@ packages: dependencies: whatwg-url: 5.0.0 + /@supabase/postgrest-js@1.15.2: + resolution: {integrity: sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==} + dependencies: + '@supabase/node-fetch': 2.6.15 + dev: true + + /@supabase/postgrest-js@1.15.4: + resolution: {integrity: sha512-Zjj3j5hVmerQRGnlo/Y9jqn7sdqRkiXf23oy5tGT5zi1jwyUDkj9vkydalY3nkgLgcBnVJDaNGzeBDr6Zn3/XQ==} + dependencies: + '@supabase/node-fetch': 2.6.15 + dev: true + /@supabase/postgrest-js@1.9.0: resolution: {integrity: sha512-axP6cU69jDrLbfihJKQ6vU27tklD0gzb9idkMN363MtTXeJVt5DQNT3JnJ58JVNBdL74hgm26rAsFNvHk+tnSw==} dependencies: @@ -4321,6 +4369,18 @@ packages: - bufferutil - utf-8-validate + /@supabase/realtime-js@2.9.5: + resolution: {integrity: sha512-TEHlGwNGGmKPdeMtca1lFTYCedrhTAv3nZVoSjrKQ+wkMmaERuCe57zkC5KSWFzLYkb5FVHW8Hrr+PX1DDwplQ==} + dependencies: + '@supabase/node-fetch': 2.6.15 + '@types/phoenix': 1.6.4 + '@types/ws': 8.5.10 + ws: 8.16.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + /@supabase/storage-js@2.5.5: resolution: {integrity: sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==} dependencies: @@ -4339,12 +4399,37 @@ packages: - bufferutil - utf-8-validate + /@supabase/supabase-js@2.43.4: + resolution: {integrity: sha512-/pLPaxiIsn5Vaz3s32HC6O/VNwfeddnzS0bZRpOW0AKcPuXroD8pT9G8mpiBlZfpKsMmq6k7tlhW7Sr1PAQ1lw==} + dependencies: + '@supabase/auth-js': 2.64.2 + '@supabase/functions-js': 2.3.1 + '@supabase/node-fetch': 2.6.15 + '@supabase/postgrest-js': 1.15.2 + '@supabase/realtime-js': 2.9.5 + '@supabase/storage-js': 2.5.5 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: false + /@swc/helpers@0.5.2: resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} dependencies: tslib: 2.5.0 dev: false + /@swc/helpers@0.5.5: + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.5.0 + dev: false + /@tanstack/match-sorter-utils@8.15.1: resolution: {integrity: sha512-PnVV3d2poenUM31ZbZi/yXkBu3J7kd5k2u51CGwwNojag451AjTH9N6n41yjXz2fpLeewleyLBmNS6+HcGDlXw==} engines: {node: '>=12'} @@ -4356,8 +4441,8 @@ packages: resolution: {integrity: sha512-Y1BpiA6BblJd/UlVqxEVeAG7IACn568YJuTTItAiecBI7En+33g780kg+/8lhgl+BzcUPN7o+NjBrSRGJoemyQ==} dev: false - /@tanstack/query-core@5.28.13: - resolution: {integrity: sha512-C3+CCOcza+mrZ7LglQbjeYEOTEC3LV0VN0eYaIN6GvqAZ8Foegdgch7n6QYPtT4FuLae5ALy+m+ZMEKpD6tMCQ==} + /@tanstack/query-core@5.38.0: + resolution: {integrity: sha512-QtkoxvFcu52mNpp3+qOo9H265m3rt83Dgbw5WnNyJvr83cegrQ7zT8haHhL4Rul6ZQkeovxyWbXVW9zI0WYx6g==} dev: false /@tanstack/react-query@5.0.0(react-dom@18.2.0)(react@18.2.0): @@ -4377,8 +4462,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@tanstack/vue-query@5.28.13(vue@3.4.21): - resolution: {integrity: sha512-MLSODlf0kYXt3YQgF8xTrF8pZ30UpIlfLNbydXsWrsE7bZaYdRnz3geQTQHD722r+O/GdNB55INesAoLZaPL5A==} + /@tanstack/vue-query@5.38.0(vue@3.4.27): + resolution: {integrity: sha512-1RTthXW8tqA3LC1HatIN7goRaL4kRdo3eOtI6TBdAl3N82VQRP2KNqa2De7Q1zLQWkHTUZFj56Ds7pMW2RtVvQ==} peerDependencies: '@vue/composition-api': ^1.1.2 vue: ^2.6.0 || ^3.3.0 @@ -4387,10 +4472,10 @@ packages: optional: true dependencies: '@tanstack/match-sorter-utils': 8.15.1 - '@tanstack/query-core': 5.28.13 + '@tanstack/query-core': 5.38.0 '@vue/devtools-api': 6.6.1 - vue: 3.4.21(typescript@5.4.2) - vue-demi: 0.14.7(vue@3.4.21) + vue: 3.4.27(typescript@5.4.5) + vue-demi: 0.14.7(vue@3.4.27) dev: false /@testing-library/dom@9.3.3: @@ -4440,8 +4525,41 @@ packages: redent: 3.0.0 dev: true - /@testing-library/react@14.2.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-7uBnPHyOG6nDGCzv8SLeJbSa33ZoYw7swYpSLIgJvBALdq7l9zPNk33om4USrxy1lKTxXaVfufzLmq83WNfWIw==} + /@testing-library/jest-dom@6.4.5(@types/jest@29.5.12)(jest@29.7.0): + resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@jest/globals': '>= 28' + '@types/bun': latest + '@types/jest': '>= 28' + jest: '>= 28' + vitest: '>= 0.32' + peerDependenciesMeta: + '@jest/globals': + optional: true + '@types/bun': + optional: true + '@types/jest': + optional: true + jest: + optional: true + vitest: + optional: true + dependencies: + '@adobe/css-tools': 4.3.2 + '@babel/runtime': 7.23.5 + '@types/jest': 29.5.12 + aria-query: 5.3.0 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + jest: 29.7.0 + lodash: 4.17.21 + redent: 3.0.0 + dev: true + + /@testing-library/react@14.3.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-AYJGvNFMbCa5vt1UtDCa/dcaABrXq8gph6VN+cffIx0UeA0qiGqS+sT60+sb+Gjc8tGXdECWYQgaF0khf8b+Lg==} engines: {node: '>=14'} peerDependencies: react: ^18.0.0 @@ -4454,8 +4572,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@testing-library/vue@8.0.3(vue@3.4.21): - resolution: {integrity: sha512-wSsbNlZ69ZFQgVlHMtc/ZC/g9BHO7MhyDrd4nHyfEubtMr3kToN/w4/BsSBknGIF8w9UmPbsgbIuq/CbdBHzCA==} + /@testing-library/vue@8.1.0(vue@3.4.27): + resolution: {integrity: sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==} engines: {node: '>=14'} peerDependencies: '@vue/compiler-sfc': '>= 3' @@ -4467,7 +4585,7 @@ packages: '@babel/runtime': 7.23.5 '@testing-library/dom': 9.3.3 '@vue/test-utils': 2.4.5 - vue: 3.4.21(typescript@5.4.2) + vue: 3.4.27(typescript@5.4.5) dev: true /@theguild/remark-mermaid@0.0.5(react@18.2.0): @@ -4563,6 +4681,10 @@ packages: resolution: {integrity: sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==} dev: true + /@types/flat@5.0.5: + resolution: {integrity: sha512-nPLljZQKSnac53KDUDzuzdRfGI0TDb5qPrb+SrQyN3MtdQrOnGsKniHN1iYZsJEBIVQve94Y6gNz22sgISZq+Q==} + dev: true + /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: @@ -4603,6 +4725,13 @@ packages: pretty-format: 29.7.0 dev: true + /@types/jest@29.5.12: + resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + /@types/js-yaml@4.0.5: resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} dev: false @@ -4767,7 +4896,7 @@ packages: typescript: optional: true dependencies: - '@eslint-community/regexpp': 4.9.1 + '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 6.8.0(eslint@8.54.0)(typescript@5.4.2) '@typescript-eslint/scope-manager': 6.8.0 '@typescript-eslint/type-utils': 6.8.0(eslint@8.54.0)(typescript@5.4.2) @@ -4778,7 +4907,7 @@ packages: graphemer: 1.4.0 ignore: 5.3.0 natural-compare: 1.4.0 - semver: 7.5.4 + semver: 7.6.0 ts-api-utils: 1.0.3(typescript@5.4.2) typescript: 5.4.2 transitivePeerDependencies: @@ -4903,7 +5032,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.0 ts-api-utils: 1.0.3(typescript@5.4.2) typescript: 5.4.2 transitivePeerDependencies: @@ -4923,7 +5052,7 @@ packages: '@typescript-eslint/types': 6.8.0 '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.4.2) eslint: 8.54.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color - typescript @@ -4955,7 +5084,7 @@ packages: react: 18.2.0 dev: false - /@vercel/analytics@1.2.2(next@14.1.0)(react@18.2.0): + /@vercel/analytics@1.2.2(next@14.2.0)(react@18.2.0): resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==} peerDependencies: next: '>= 13' @@ -4966,78 +5095,78 @@ packages: react: optional: true dependencies: - next: 14.1.0(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 server-only: 0.0.1 dev: false - /@vue/compiler-core@3.4.21: - resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==} + /@vue/compiler-core@3.4.27: + resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==} dependencies: - '@babel/parser': 7.24.0 - '@vue/shared': 3.4.21 + '@babel/parser': 7.24.6 + '@vue/shared': 3.4.27 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.0 - /@vue/compiler-dom@3.4.21: - resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} + /@vue/compiler-dom@3.4.27: + resolution: {integrity: sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==} dependencies: - '@vue/compiler-core': 3.4.21 - '@vue/shared': 3.4.21 + '@vue/compiler-core': 3.4.27 + '@vue/shared': 3.4.27 - /@vue/compiler-sfc@3.4.21: - resolution: {integrity: sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==} + /@vue/compiler-sfc@3.4.27: + resolution: {integrity: sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==} dependencies: - '@babel/parser': 7.24.0 - '@vue/compiler-core': 3.4.21 - '@vue/compiler-dom': 3.4.21 - '@vue/compiler-ssr': 3.4.21 - '@vue/shared': 3.4.21 + '@babel/parser': 7.24.6 + '@vue/compiler-core': 3.4.27 + '@vue/compiler-dom': 3.4.27 + '@vue/compiler-ssr': 3.4.27 + '@vue/shared': 3.4.27 estree-walker: 2.0.2 - magic-string: 0.30.9 + magic-string: 0.30.10 postcss: 8.4.38 source-map-js: 1.2.0 - /@vue/compiler-ssr@3.4.21: - resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==} + /@vue/compiler-ssr@3.4.27: + resolution: {integrity: sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==} dependencies: - '@vue/compiler-dom': 3.4.21 - '@vue/shared': 3.4.21 + '@vue/compiler-dom': 3.4.27 + '@vue/shared': 3.4.27 /@vue/devtools-api@6.6.1: resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==} dev: false - /@vue/reactivity@3.4.21: - resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==} + /@vue/reactivity@3.4.27: + resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==} dependencies: - '@vue/shared': 3.4.21 + '@vue/shared': 3.4.27 - /@vue/runtime-core@3.4.21: - resolution: {integrity: sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==} + /@vue/runtime-core@3.4.27: + resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==} dependencies: - '@vue/reactivity': 3.4.21 - '@vue/shared': 3.4.21 + '@vue/reactivity': 3.4.27 + '@vue/shared': 3.4.27 - /@vue/runtime-dom@3.4.21: - resolution: {integrity: sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==} + /@vue/runtime-dom@3.4.27: + resolution: {integrity: sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==} dependencies: - '@vue/runtime-core': 3.4.21 - '@vue/shared': 3.4.21 + '@vue/runtime-core': 3.4.27 + '@vue/shared': 3.4.27 csstype: 3.1.3 - /@vue/server-renderer@3.4.21(vue@3.4.21): - resolution: {integrity: sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==} + /@vue/server-renderer@3.4.27(vue@3.4.27): + resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==} peerDependencies: - vue: 3.4.21 + vue: 3.4.27 dependencies: - '@vue/compiler-ssr': 3.4.21 - '@vue/shared': 3.4.21 - vue: 3.4.21(typescript@5.4.2) + '@vue/compiler-ssr': 3.4.27 + '@vue/shared': 3.4.27 + vue: 3.4.27(typescript@5.4.5) - /@vue/shared@3.4.21: - resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==} + /@vue/shared@3.4.27: + resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} /@vue/test-utils@2.4.5: resolution: {integrity: sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==} @@ -5600,10 +5729,6 @@ packages: /caniuse-lite@1.0.30001551: resolution: {integrity: sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==} - /caniuse-lite@1.0.30001579: - resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} - dev: false - /caniuse-lite@1.0.30001599: resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==} @@ -6528,6 +6653,11 @@ packages: engines: {node: '>=12'} dev: true + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + dev: true + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -6724,6 +6854,7 @@ packages: '@esbuild/win32-arm64': 0.19.8 '@esbuild/win32-ia32': 0.19.8 '@esbuild/win32-x64': 0.19.8 + dev: true /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -6783,8 +6914,8 @@ packages: - supports-color dev: true - /eslint-config-next@14.1.0(eslint@8.54.0)(typescript@5.4.2): - resolution: {integrity: sha512-SBX2ed7DoRFXC6CQSLc/SbLY9Ut6HxNB2wPTcoIWjUMd7aF7O/SIE7111L8FdZ9TXsNV4pulUDnfthpyPtbFUg==} + /eslint-config-next@14.2.0(eslint@8.54.0)(typescript@5.4.2): + resolution: {integrity: sha512-N0eQkn/wz557mIpW4JQWGEv4wGU8zvJ7emLHMS15uC18jjaU4kx6leR4U9QYT/eNghUZT7N9lBlfd8E4N0cp1w==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 typescript: '>=3.3.1' @@ -6792,7 +6923,7 @@ packages: typescript: optional: true dependencies: - '@next/eslint-plugin-next': 14.1.0 + '@next/eslint-plugin-next': 14.2.0 '@rushstack/eslint-patch': 1.4.0 '@typescript-eslint/parser': 6.8.0(eslint@8.54.0)(typescript@5.4.2) eslint: 8.54.0 @@ -6835,8 +6966,8 @@ packages: eslint-plugin-turbo: 1.10.16(eslint@8.54.0) dev: false - /eslint-config-universe@12.0.0(eslint@8.54.0)(prettier@3.2.0)(typescript@5.4.2): - resolution: {integrity: sha512-78UxGByheyDNL1RhszWYeDzWiBaUtLnFSeI20pJI89IXa9OAEZQHzG/iBFpMeaCs7Hqyg0wYJcuCbCx535wB7A==} + /eslint-config-universe@12.1.0(eslint@8.54.0)(prettier@3.2.0)(typescript@5.4.2): + resolution: {integrity: sha512-nAT0/rcOyYvsy0tY1yq0/BchUQImRCpORONzCvLkjixRBH5CbkMddb/V8ZlOLPgkfu3lp+sByRmSXNfoupTj2w==} peerDependencies: eslint: '>=8.10' prettier: '>=3' @@ -7169,8 +7300,8 @@ packages: eslint: 8.54.0 dev: false - /eslint-plugin-vue@9.25.0(eslint@8.54.0): - resolution: {integrity: sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA==} + /eslint-plugin-vue@9.26.0(eslint@8.54.0): + resolution: {integrity: sha512-eTvlxXgd4ijE1cdur850G6KalZqk65k1JKoOI2d1kT3hr8sPD07j1q98FRFdNnpxBELGPWxZmInxeHGF/GxtqQ==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 @@ -9174,9 +9305,8 @@ packages: hasBin: true dev: true - /magic-string@0.30.9: - resolution: {integrity: sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==} - engines: {node: '>=12'} + /magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -10016,14 +10146,14 @@ packages: - supports-color dev: false - /next-seo@6.4.0(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): + /next-seo@6.4.0(next@14.2.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-XQFxkOL2hw0YE+P100HbI3EAvcludlHPxuzMgaIjKb7kPK0CvjGvLFjd9hszZFEDc5oiQkGFA8+cuWcnip7eYA==} peerDependencies: next: ^8.1.1-canary.54 || >=9.0.0 react: '>=16.0.0' react-dom: '>=16.0.0' dependencies: - next: 14.1.0(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -10040,14 +10170,14 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /next-themes@0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): + /next-themes@0.2.1(next@14.2.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: next: '*' react: '*' react-dom: '*' dependencies: - next: 14.1.0(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -10091,46 +10221,49 @@ packages: - babel-plugin-macros dev: false - /next@14.1.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==} + /next@14.2.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2T41HqJdKPqheR27ll7MFZ3gtTYvGew7cUc0PwPSyK9Ao5vvwpf9bYfP4V5YBGLckHF2kEGvrLte5BqLSv0s8g==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 react: ^18.2.0 react-dom: ^18.2.0 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': optional: true + '@playwright/test': + optional: true sass: optional: true dependencies: - '@next/env': 14.1.0 - '@swc/helpers': 0.5.2 + '@next/env': 14.2.0 + '@swc/helpers': 0.5.5 busboy: 1.6.0 - caniuse-lite: 1.0.30001579 + caniuse-lite: 1.0.30001599 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) styled-jsx: 5.1.1(@babel/core@7.23.2)(react@18.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 14.1.0 - '@next/swc-darwin-x64': 14.1.0 - '@next/swc-linux-arm64-gnu': 14.1.0 - '@next/swc-linux-arm64-musl': 14.1.0 - '@next/swc-linux-x64-gnu': 14.1.0 - '@next/swc-linux-x64-musl': 14.1.0 - '@next/swc-win32-arm64-msvc': 14.1.0 - '@next/swc-win32-ia32-msvc': 14.1.0 - '@next/swc-win32-x64-msvc': 14.1.0 + '@next/swc-darwin-arm64': 14.2.0 + '@next/swc-darwin-x64': 14.2.0 + '@next/swc-linux-arm64-gnu': 14.2.0 + '@next/swc-linux-arm64-musl': 14.2.0 + '@next/swc-linux-x64-gnu': 14.2.0 + '@next/swc-linux-x64-musl': 14.2.0 + '@next/swc-win32-arm64-msvc': 14.2.0 + '@next/swc-win32-ia32-msvc': 14.2.0 + '@next/swc-win32-x64-msvc': 14.2.0 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros dev: false - /nextra-theme-docs@2.13.2(next@14.1.0)(nextra@2.13.2)(react-dom@18.2.0)(react@18.2.0): + /nextra-theme-docs@2.13.2(next@14.2.0)(nextra@2.13.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yE4umXaImp1/kf/sFciPj2+EFrNSwd9Db26hi98sIIiujzGf3+9eUgAz45vF9CwBw50FSXxm1QGRcY+slQ4xQQ==} peerDependencies: next: '>=9.5.3' @@ -10147,17 +10280,17 @@ packages: git-url-parse: 13.1.0 intersection-observer: 0.12.2 match-sorter: 6.3.1 - next: 14.1.0(react-dom@18.2.0)(react@18.2.0) - next-seo: 6.4.0(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) - next-themes: 0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) - nextra: 2.13.2(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0(react-dom@18.2.0)(react@18.2.0) + next-seo: 6.4.0(next@14.2.0)(react-dom@18.2.0)(react@18.2.0) + next-themes: 0.2.1(next@14.2.0)(react-dom@18.2.0)(react@18.2.0) + nextra: 2.13.2(next@14.2.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) scroll-into-view-if-needed: 3.0.6 zod: 3.22.4 dev: false - /nextra@2.13.2(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): + /nextra@2.13.2(next@14.2.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pIgOSXNUqTz1laxV4ChFZOU7lzJAoDHHaBPj8L09PuxrLKqU1BU/iZtXAG6bQeKCx8EPdBsoXxEuENnL9QGnGA==} engines: {node: '>=16'} peerDependencies: @@ -10177,7 +10310,7 @@ packages: gray-matter: 4.0.3 katex: 0.16.9 lodash.get: 4.4.2 - next: 14.1.0(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0(react-dom@18.2.0)(react@18.2.0) next-mdx-remote: 4.4.1(react-dom@18.2.0)(react@18.2.0) p-limit: 3.1.0 react: 18.2.0 @@ -12128,6 +12261,78 @@ packages: semver: 7.5.4 typescript: 5.4.2 yargs-parser: 21.1.1 + dev: true + + /ts-jest@29.1.0(jest@29.7.0)(typescript@5.4.5): + resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0 + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 5.4.5 + yargs-parser: 21.1.1 + dev: false + + /ts-jest@29.1.3(@babel/core@7.24.0)(esbuild@0.19.8)(jest@29.7.0)(typescript@5.4.5): + resolution: {integrity: sha512-6L9qz3ginTd1NKhOxmkP0qU3FyKjj5CPoY+anszfVn6Pmv/RIKzhiMCsH7Yb7UvJR9I2A64rm4zQl531s2F1iw==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.24.0 + bs-logger: 0.2.6 + esbuild: 0.19.8 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0 + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.0 + typescript: 5.4.5 + yargs-parser: 21.1.1 + dev: true /tsconfig-paths@3.14.2: resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} @@ -12182,6 +12387,45 @@ packages: - ts-node dev: true + /tsup@8.0.2(typescript@5.4.5): + resolution: {integrity: sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.0.2(esbuild@0.19.8) + cac: 6.7.14 + chokidar: 3.5.3 + debug: 4.3.4 + esbuild: 0.19.8 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2(postcss@8.4.31) + resolve-from: 5.0.0 + rollup: 4.6.1 + source-map: 0.8.0-beta.0 + sucrase: 3.34.0 + tree-kill: 1.2.2 + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + /tsutils@3.21.0(typescript@5.4.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -12348,6 +12592,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + /typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + engines: {node: '>=14.17'} + hasBin: true + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -12739,7 +12988,7 @@ packages: resolution: {integrity: sha512-FC5fKJjDks3Ue/KRSYBdsiCaZa0kUPQfs8yQpb8W9mlO6BenV8G1z58xobeRMzevnmEcDa09LLwuXDwb4f6NMQ==} dev: true - /vue-demi@0.14.7(vue@3.4.21): + /vue-demi@0.14.7(vue@3.4.27): resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} engines: {node: '>=12'} hasBin: true @@ -12751,7 +13000,7 @@ packages: '@vue/composition-api': optional: true dependencies: - vue: 3.4.21(typescript@5.4.2) + vue: 3.4.27(typescript@5.4.5) dev: false /vue-eslint-parser@9.4.2(eslint@8.54.0): @@ -12772,20 +13021,20 @@ packages: - supports-color dev: true - /vue@3.4.21(typescript@5.4.2): - resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==} + /vue@3.4.27(typescript@5.4.5): + resolution: {integrity: sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@vue/compiler-dom': 3.4.21 - '@vue/compiler-sfc': 3.4.21 - '@vue/runtime-dom': 3.4.21 - '@vue/server-renderer': 3.4.21(vue@3.4.21) - '@vue/shared': 3.4.21 - typescript: 5.4.2 + '@vue/compiler-dom': 3.4.27 + '@vue/compiler-sfc': 3.4.27 + '@vue/runtime-dom': 3.4.27 + '@vue/server-renderer': 3.4.27(vue@3.4.27) + '@vue/shared': 3.4.27 + typescript: 5.4.5 /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}