@supatype/vue 0.1.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+ import { useSupatype } from "./context.js";
3
+ /**
4
+ * Real-time subscription composable.
5
+ *
6
+ * @example
7
+ * ```vue
8
+ * <script setup>
9
+ * import { useSubscription } from '@supatype/vue'
10
+ *
11
+ * const { data: messages, status } = useSubscription('messages', {
12
+ * event: '*',
13
+ * filter: 'room_id=eq.123',
14
+ * })
15
+ * </script>
16
+ * ```
17
+ */
18
+ export function useSubscription(table, options) {
19
+ const client = useSupatype();
20
+ const data = ref(null);
21
+ const error = ref(null);
22
+ const status = ref("connecting");
23
+ let unsubscribe = null;
24
+ onMounted(() => {
25
+ const event = options?.event ?? "*";
26
+ const channelOpts = {
27
+ event,
28
+ schema: "public",
29
+ table,
30
+ };
31
+ if (options?.filter) {
32
+ channelOpts.filter = options.filter;
33
+ }
34
+ const channel = client.realtime.channel(`public:${table}`);
35
+ channel.on("postgres_changes", channelOpts, (payload) => {
36
+ if (!data.value)
37
+ data.value = [];
38
+ if (payload.eventType === "INSERT") {
39
+ data.value = [...data.value, payload.new];
40
+ }
41
+ else if (payload.eventType === "UPDATE") {
42
+ data.value = data.value.map((row) => {
43
+ const r = row;
44
+ const n = payload.new;
45
+ return r["id"] === n["id"] ? payload.new : row;
46
+ });
47
+ }
48
+ else if (payload.eventType === "DELETE") {
49
+ const old = payload.old;
50
+ data.value = data.value.filter((row) => row["id"] !== old["id"]);
51
+ }
52
+ });
53
+ channel.subscribe((newStatus) => {
54
+ if (newStatus === "SUBSCRIBED") {
55
+ status.value = "connected";
56
+ }
57
+ else if (newStatus === "CHANNEL_ERROR") {
58
+ status.value = "error";
59
+ error.value = { message: "Subscription error" };
60
+ }
61
+ else if (newStatus === "CLOSED") {
62
+ status.value = "disconnected";
63
+ }
64
+ });
65
+ unsubscribe = () => {
66
+ channel.unsubscribe();
67
+ };
68
+ });
69
+ onUnmounted(() => {
70
+ unsubscribe?.();
71
+ });
72
+ return { data, error, status };
73
+ }
74
+ //# sourceMappingURL=useSubscription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSubscription.js","sourceRoot":"","sources":["../src/useSubscription.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@supatype/vue",
3
+ "version": "0.1.0-alpha.9",
4
+ "description": "Vue composables for Supatype — useQuery, useMutation, useAuth, useSubscription",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "peerDependencies": {
15
+ "vue": "^3.3.0",
16
+ "@supatype/client": "0.1.0-alpha.9"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5",
20
+ "vue": "^3.5.0",
21
+ "@supatype/client": "0.1.0-alpha.9"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc --project tsconfig.json",
25
+ "typecheck": "tsc --project tsconfig.json --noEmit",
26
+ "clean": "rm -rf dist *.tsbuildinfo"
27
+ }
28
+ }
package/src/context.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Supatype Vue plugin and injection key.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { createApp } from 'vue'
7
+ * import { createClient } from '@supatype/client'
8
+ * import { supatypePlugin } from '@supatype/vue'
9
+ *
10
+ * const supatype = createClient({ url: '...', anonKey: '...' })
11
+ * const app = createApp(App)
12
+ * app.use(supatypePlugin, supatype)
13
+ * ```
14
+ */
15
+
16
+ import { inject, type InjectionKey, type Plugin } from "vue"
17
+ import type { SupatypeClient, AnyDatabase } from "@supatype/client"
18
+
19
+ export const SUPATYPE_KEY: InjectionKey<SupatypeClient> = Symbol("supatype")
20
+
21
+ /**
22
+ * Vue plugin that provides the Supatype client to all components.
23
+ */
24
+ export const supatypePlugin: Plugin = {
25
+ install(app, client: SupatypeClient) {
26
+ app.provide(SUPATYPE_KEY, client)
27
+ },
28
+ }
29
+
30
+ /**
31
+ * Get the Supatype client from the injection context.
32
+ * Must be called inside setup() of a component that has the supatypePlugin installed.
33
+ */
34
+ export function useSupatype<TDatabase extends AnyDatabase = AnyDatabase>(): SupatypeClient<TDatabase> {
35
+ const client = inject(SUPATYPE_KEY)
36
+ if (!client) {
37
+ throw new Error("useSupatype() requires the supatypePlugin to be installed on the Vue app.")
38
+ }
39
+ return client as SupatypeClient<TDatabase>
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ // @supatype/vue — Vue composables for Supatype
2
+
3
+ export { supatypePlugin, useSupatype, SUPATYPE_KEY } from "./context.js"
4
+
5
+ export { useQuery } from "./useQuery.js"
6
+ export type { UseQueryOptions, UseQueryReturn } from "./useQuery.js"
7
+
8
+ export { useMutation } from "./useMutation.js"
9
+ export type { UseMutationReturn, MutationOperation, MutationOptions } from "./useMutation.js"
10
+
11
+ export { useAuth } from "./useAuth.js"
12
+ export type { UseAuthReturn } from "./useAuth.js"
13
+
14
+ export { useSubscription } from "./useSubscription.js"
15
+ export type { UseSubscriptionOptions, UseSubscriptionReturn, SubscriptionEvent } from "./useSubscription.js"
16
+
17
+ export { useFunction } from "./useFunction.js"
18
+ export type { UseFunctionReturn } from "./useFunction.js"
package/src/useAuth.ts ADDED
@@ -0,0 +1,96 @@
1
+ import { ref, onMounted, onUnmounted, type Ref } from "vue"
2
+ import type { AnyDatabase, SupatypeError, User, Session, AuthChangeEvent } from "@supatype/client"
3
+ import { useSupatype } from "./context.js"
4
+
5
+ export interface UseAuthReturn {
6
+ user: Ref<User | null>
7
+ session: Ref<Session | null>
8
+ loading: Ref<boolean>
9
+ signIn: (email: string, password: string) => Promise<{ error: SupatypeError | null }>
10
+ signUp: (email: string, password: string) => Promise<{ error: SupatypeError | null }>
11
+ signOut: () => Promise<{ error: SupatypeError | null }>
12
+ signInWithOAuth: (provider: string) => Promise<{ error: SupatypeError | null }>
13
+ resetPassword: (email: string) => Promise<{ error: SupatypeError | null }>
14
+ }
15
+
16
+ /**
17
+ * Authentication composable for Supatype.
18
+ *
19
+ * @example
20
+ * ```vue
21
+ * <script setup>
22
+ * import { useAuth } from '@supatype/vue'
23
+ *
24
+ * const { user, loading, signIn, signOut } = useAuth()
25
+ * </script>
26
+ *
27
+ * <template>
28
+ * <div v-if="loading">Loading...</div>
29
+ * <div v-else-if="user">
30
+ * <p>Signed in as {{ user.email }}</p>
31
+ * <button @click="signOut">Sign out</button>
32
+ * </div>
33
+ * <form v-else @submit.prevent="signIn(email, password)">...</form>
34
+ * </template>
35
+ * ```
36
+ */
37
+ export function useAuth<TDatabase extends AnyDatabase = AnyDatabase>(): UseAuthReturn {
38
+ const client = useSupatype<TDatabase>()
39
+ const user = ref<User | null>(null) as Ref<User | null>
40
+ const session = ref<Session | null>(null) as Ref<Session | null>
41
+ const loading = ref(true)
42
+
43
+ let unsubscribe: (() => void) | null = null
44
+
45
+ onMounted(async () => {
46
+ // Get initial session
47
+ try {
48
+ const { data } = await client.auth.getSession()
49
+ if (data.session) {
50
+ session.value = data.session
51
+ user.value = data.session.user
52
+ }
53
+ } catch {
54
+ // Ignore — user is not authenticated
55
+ }
56
+ loading.value = false
57
+
58
+ // Subscribe to auth changes
59
+ const { data: { subscription } } = client.auth.onAuthStateChange((event: AuthChangeEvent, newSession: Session | null) => {
60
+ session.value = newSession
61
+ user.value = newSession?.user ?? null
62
+ })
63
+ unsubscribe = () => subscription.unsubscribe()
64
+ })
65
+
66
+ onUnmounted(() => {
67
+ unsubscribe?.()
68
+ })
69
+
70
+ const signIn = async (email: string, password: string) => {
71
+ const { error } = await client.auth.signInWithPassword({ email, password })
72
+ return { error }
73
+ }
74
+
75
+ const signUp = async (email: string, password: string) => {
76
+ const { error } = await client.auth.signUp({ email, password })
77
+ return { error }
78
+ }
79
+
80
+ const signOut = async () => {
81
+ const { error } = await client.auth.signOut()
82
+ return { error }
83
+ }
84
+
85
+ const signInWithOAuth = async (provider: string) => {
86
+ const { error } = await client.auth.signInWithOAuth({ provider })
87
+ return { error }
88
+ }
89
+
90
+ const resetPassword = async (email: string) => {
91
+ const { error } = await client.auth.resetPasswordForEmail(email)
92
+ return { error }
93
+ }
94
+
95
+ return { user, session, loading, signIn, signUp, signOut, signInWithOAuth, resetPassword }
96
+ }
@@ -0,0 +1,54 @@
1
+ import { ref, type Ref } from "vue"
2
+ import type { AnyDatabase, SupatypeError } from "@supatype/client"
3
+ import { useSupatype } from "./context.js"
4
+
5
+ export interface UseFunctionReturn<TResponse> {
6
+ invoke: (body?: unknown) => Promise<{ data: TResponse | null; error: SupatypeError | null }>
7
+ data: Ref<TResponse | null>
8
+ error: Ref<SupatypeError | null>
9
+ loading: Ref<boolean>
10
+ }
11
+
12
+ /**
13
+ * Edge function invocation composable.
14
+ *
15
+ * @example
16
+ * ```vue
17
+ * <script setup>
18
+ * import { useFunction } from '@supatype/vue'
19
+ *
20
+ * const { invoke, data, loading } = useFunction<{ orderId: string }>('process-order')
21
+ *
22
+ * async function handleSubmit() {
23
+ * await invoke({ items: cart.value, address: address.value })
24
+ * }
25
+ * </script>
26
+ * ```
27
+ */
28
+ export function useFunction<
29
+ TResponse = unknown,
30
+ TDatabase extends AnyDatabase = AnyDatabase,
31
+ >(
32
+ functionName: string,
33
+ ): UseFunctionReturn<TResponse> {
34
+ const client = useSupatype<TDatabase>()
35
+ const data = ref<TResponse | null>(null) as Ref<TResponse | null>
36
+ const error = ref<SupatypeError | null>(null)
37
+ const loading = ref(false)
38
+
39
+ const invoke = async (body?: unknown) => {
40
+ loading.value = true
41
+ error.value = null
42
+
43
+ const result = await client.functions.invoke(functionName, {
44
+ ...(body !== undefined ? { body } : {}),
45
+ })
46
+
47
+ loading.value = false
48
+ data.value = result.data as TResponse | null
49
+ if (result.error) error.value = result.error
50
+ return result as { data: TResponse | null; error: SupatypeError | null }
51
+ }
52
+
53
+ return { invoke, data, error, loading }
54
+ }
@@ -0,0 +1,94 @@
1
+ import { ref, type Ref } from "vue"
2
+ import type { AnyDatabase, SupatypeError } from "@supatype/client"
3
+ import { useSupatype } from "./context.js"
4
+
5
+ export type MutationOperation = "insert" | "update" | "delete" | "upsert"
6
+
7
+ export interface MutationOptions {
8
+ filter?: Record<string, unknown> | undefined
9
+ }
10
+
11
+ export interface UseMutationReturn<TRow> {
12
+ mutate: (
13
+ data?: Record<string, unknown> | Record<string, unknown>[] | undefined,
14
+ options?: MutationOptions | undefined,
15
+ ) => Promise<{ data: TRow[] | null; error: SupatypeError | null }>
16
+ loading: Ref<boolean>
17
+ error: Ref<SupatypeError | null>
18
+ }
19
+
20
+ /**
21
+ * Mutation composable for insert, update, delete, and upsert operations.
22
+ *
23
+ * @example
24
+ * ```vue
25
+ * <script setup>
26
+ * import { useMutation } from '@supatype/vue'
27
+ *
28
+ * const { mutate: createPost, loading } = useMutation('posts', 'insert')
29
+ *
30
+ * async function handleSubmit() {
31
+ * const { data, error } = await createPost({ title: 'Hello', status: 'draft' })
32
+ * }
33
+ * </script>
34
+ * ```
35
+ */
36
+ export function useMutation<
37
+ TDatabase extends AnyDatabase = AnyDatabase,
38
+ TTable extends keyof TDatabase["public"]["Tables"] & string = keyof TDatabase["public"]["Tables"] & string,
39
+ TRow = TDatabase["public"]["Tables"][TTable]["Row"],
40
+ >(
41
+ table: TTable,
42
+ operation: MutationOperation,
43
+ ): UseMutationReturn<TRow> {
44
+ const client = useSupatype<TDatabase>()
45
+ const loading = ref(false)
46
+ const error = ref<SupatypeError | null>(null)
47
+
48
+ const mutate = async (
49
+ data?: Record<string, unknown> | Record<string, unknown>[] | undefined,
50
+ options?: MutationOptions | undefined,
51
+ ) => {
52
+ loading.value = true
53
+ error.value = null
54
+
55
+ try {
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ const tableClient = client.from(table) as any
58
+ let result: { data: TRow[] | null; error: SupatypeError | null }
59
+
60
+ if (operation === "insert") {
61
+ result = await tableClient.insert(data)
62
+ } else if (operation === "upsert") {
63
+ result = await tableClient.upsert(data)
64
+ } else if (operation === "update") {
65
+ let q = tableClient.update(data)
66
+ if (options?.filter) {
67
+ for (const [col, val] of Object.entries(options.filter)) {
68
+ q = q.eq(col, val)
69
+ }
70
+ }
71
+ result = await q
72
+ } else {
73
+ let q = tableClient.delete()
74
+ if (options?.filter) {
75
+ for (const [col, val] of Object.entries(options.filter)) {
76
+ q = q.eq(col, val)
77
+ }
78
+ }
79
+ result = await q
80
+ }
81
+
82
+ if (result.error) error.value = result.error
83
+ return result
84
+ } catch (e) {
85
+ const err = { message: e instanceof Error ? e.message : "Unknown error" }
86
+ error.value = err
87
+ return { data: null, error: err }
88
+ } finally {
89
+ loading.value = false
90
+ }
91
+ }
92
+
93
+ return { mutate, loading, error }
94
+ }
@@ -0,0 +1,106 @@
1
+ import { ref, onMounted, watch, type Ref } from "vue"
2
+ import type { AnyDatabase, SupatypeError } from "@supatype/client"
3
+ import { useSupatype } from "./context.js"
4
+
5
+ export interface UseQueryOptions {
6
+ columns?: string | undefined
7
+ filter?: Record<string, unknown> | undefined
8
+ order?: { column: string; ascending?: boolean } | undefined
9
+ limit?: number | undefined
10
+ offset?: number | undefined
11
+ enabled?: Ref<boolean> | boolean | undefined
12
+ }
13
+
14
+ export interface UseQueryReturn<TRow> {
15
+ data: Ref<TRow[] | null>
16
+ error: Ref<SupatypeError | null>
17
+ loading: Ref<boolean>
18
+ refetch: () => Promise<void>
19
+ }
20
+
21
+ /**
22
+ * Reactive data fetching composable for Supatype tables.
23
+ *
24
+ * @example
25
+ * ```vue
26
+ * <script setup>
27
+ * import { useQuery } from '@supatype/vue'
28
+ *
29
+ * const { data: posts, loading, error, refetch } = useQuery('posts', {
30
+ * order: { column: 'created_at', ascending: false },
31
+ * limit: 10,
32
+ * })
33
+ * </script>
34
+ * ```
35
+ */
36
+ export function useQuery<
37
+ TDatabase extends AnyDatabase = AnyDatabase,
38
+ TTable extends keyof TDatabase["public"]["Tables"] & string = keyof TDatabase["public"]["Tables"] & string,
39
+ TRow = TDatabase["public"]["Tables"][TTable]["Row"],
40
+ >(
41
+ table: TTable,
42
+ options?: UseQueryOptions | undefined,
43
+ ): UseQueryReturn<TRow> {
44
+ const client = useSupatype<TDatabase>()
45
+ const data = ref<TRow[] | null>(null) as Ref<TRow[] | null>
46
+ const error = ref<SupatypeError | null>(null)
47
+ const loading = ref(false)
48
+
49
+ const fetchData = async () => {
50
+ // Check enabled
51
+ const enabled = options?.enabled
52
+ if (enabled !== undefined) {
53
+ const isEnabled = typeof enabled === "boolean" ? enabled : enabled.value
54
+ if (!isEnabled) return
55
+ }
56
+
57
+ loading.value = true
58
+ error.value = null
59
+
60
+ try {
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ let query = (client.from(table) as any).select(options?.columns)
63
+
64
+ if (options?.filter) {
65
+ for (const [col, val] of Object.entries(options.filter)) {
66
+ query = query.eq(col, val)
67
+ }
68
+ }
69
+
70
+ if (options?.order) {
71
+ query = query.order(options.order.column, {
72
+ ascending: options.order.ascending ?? true,
73
+ })
74
+ }
75
+
76
+ if (options?.limit !== undefined) {
77
+ query = query.limit(options.limit)
78
+ }
79
+
80
+ if (options?.offset !== undefined) {
81
+ query = query.range(options.offset, options.offset + (options.limit ?? 100) - 1)
82
+ }
83
+
84
+ const result = await query
85
+ data.value = result.data as TRow[] | null
86
+ if (result.error) {
87
+ error.value = result.error
88
+ }
89
+ } catch (e) {
90
+ error.value = { message: e instanceof Error ? e.message : "Unknown error" }
91
+ } finally {
92
+ loading.value = false
93
+ }
94
+ }
95
+
96
+ onMounted(fetchData)
97
+
98
+ // Re-fetch when enabled changes
99
+ if (options?.enabled && typeof options.enabled !== "boolean") {
100
+ watch(options.enabled, (newVal) => {
101
+ if (newVal) fetchData()
102
+ })
103
+ }
104
+
105
+ return { data, error, loading, refetch: fetchData }
106
+ }
@@ -0,0 +1,104 @@
1
+ import { ref, onMounted, onUnmounted, type Ref } from "vue"
2
+ import type { AnyDatabase, SupatypeError, RealtimePayload, RealtimeEvent, ChannelStatus } from "@supatype/client"
3
+ import { useSupatype } from "./context.js"
4
+
5
+ export type SubscriptionEvent = RealtimeEvent
6
+
7
+ export interface UseSubscriptionOptions {
8
+ event?: SubscriptionEvent | undefined
9
+ filter?: string | undefined
10
+ }
11
+
12
+ export interface UseSubscriptionReturn<TRow> {
13
+ data: Ref<TRow[] | null>
14
+ error: Ref<SupatypeError | null>
15
+ status: Ref<"connecting" | "connected" | "disconnected" | "error">
16
+ }
17
+
18
+ /**
19
+ * Real-time subscription composable.
20
+ *
21
+ * @example
22
+ * ```vue
23
+ * <script setup>
24
+ * import { useSubscription } from '@supatype/vue'
25
+ *
26
+ * const { data: messages, status } = useSubscription('messages', {
27
+ * event: '*',
28
+ * filter: 'room_id=eq.123',
29
+ * })
30
+ * </script>
31
+ * ```
32
+ */
33
+ export function useSubscription<
34
+ TDatabase extends AnyDatabase = AnyDatabase,
35
+ TTable extends keyof TDatabase["public"]["Tables"] & string = keyof TDatabase["public"]["Tables"] & string,
36
+ TRow = TDatabase["public"]["Tables"][TTable]["Row"],
37
+ >(
38
+ table: TTable,
39
+ options?: UseSubscriptionOptions | undefined,
40
+ ): UseSubscriptionReturn<TRow> {
41
+ const client = useSupatype<TDatabase>()
42
+ const data = ref<TRow[] | null>(null) as Ref<TRow[] | null>
43
+ const error = ref<SupatypeError | null>(null)
44
+ const status = ref<"connecting" | "connected" | "disconnected" | "error">("connecting")
45
+
46
+ let unsubscribe: (() => void) | null = null
47
+
48
+ onMounted(() => {
49
+ const event: RealtimeEvent = (options?.event as RealtimeEvent) ?? "*"
50
+ const channelOpts: {
51
+ event: RealtimeEvent
52
+ schema: string
53
+ table: string
54
+ filter?: string | undefined
55
+ } = {
56
+ event,
57
+ schema: "public",
58
+ table,
59
+ }
60
+ if (options?.filter) {
61
+ channelOpts.filter = options.filter
62
+ }
63
+
64
+ const channel = client.realtime.channel<TRow>(`public:${table}`)
65
+
66
+ channel.on("postgres_changes", channelOpts, (payload: RealtimePayload<TRow>) => {
67
+ if (!data.value) data.value = []
68
+
69
+ if (payload.eventType === "INSERT") {
70
+ data.value = [...data.value, payload.new as TRow]
71
+ } else if (payload.eventType === "UPDATE") {
72
+ data.value = data.value.map((row) => {
73
+ const r = row as Record<string, unknown>
74
+ const n = payload.new as Record<string, unknown>
75
+ return r["id"] === n["id"] ? (payload.new as TRow) : row
76
+ })
77
+ } else if (payload.eventType === "DELETE") {
78
+ const old = payload.old as Record<string, unknown>
79
+ data.value = data.value.filter((row) => (row as Record<string, unknown>)["id"] !== old["id"])
80
+ }
81
+ })
82
+
83
+ channel.subscribe((newStatus: ChannelStatus) => {
84
+ if (newStatus === "SUBSCRIBED") {
85
+ status.value = "connected"
86
+ } else if (newStatus === "CHANNEL_ERROR") {
87
+ status.value = "error"
88
+ error.value = { message: "Subscription error" }
89
+ } else if (newStatus === "CLOSED") {
90
+ status.value = "disconnected"
91
+ }
92
+ })
93
+
94
+ unsubscribe = () => {
95
+ channel.unsubscribe()
96
+ }
97
+ })
98
+
99
+ onUnmounted(() => {
100
+ unsubscribe?.()
101
+ })
102
+
103
+ return { data, error, status }
104
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "composite": true,
7
+ "lib": ["ES2022", "DOM"]
8
+ },
9
+ "include": ["src"],
10
+ "references": [
11
+ { "path": "../client" }
12
+ ]
13
+ }