@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.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +36 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/useAuth.d.ts +45 -0
- package/dist/useAuth.d.ts.map +1 -0
- package/dist/useAuth.js +75 -0
- package/dist/useAuth.js.map +1 -0
- package/dist/useFunction.d.ts +29 -0
- package/dist/useFunction.d.ts.map +1 -0
- package/dist/useFunction.js +38 -0
- package/dist/useFunction.js.map +1 -0
- package/dist/useMutation.d.ts +32 -0
- package/dist/useMutation.d.ts.map +1 -0
- package/dist/useMutation.js +69 -0
- package/dist/useMutation.js.map +1 -0
- package/dist/useQuery.d.ts +36 -0
- package/dist/useQuery.d.ts.map +1 -0
- package/dist/useQuery.js +75 -0
- package/dist/useQuery.js.map +1 -0
- package/dist/useSubscription.d.ts +29 -0
- package/dist/useSubscription.d.ts.map +1 -0
- package/dist/useSubscription.js +74 -0
- package/dist/useSubscription.js.map +1 -0
- package/package.json +28 -0
- package/src/context.ts +40 -0
- package/src/index.ts +18 -0
- package/src/useAuth.ts +96 -0
- package/src/useFunction.ts +54 -0
- package/src/useMutation.ts +94 -0
- package/src/useQuery.ts +106 -0
- package/src/useSubscription.ts +104 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -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
|
+
}
|
package/src/useQuery.ts
ADDED
|
@@ -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