git.iko.soy/rowtype-yoga/purescript-supabase
on main help sign in
13 files changed+196−16
since splitsnapshots
unifiedsplit
spago.dhall+4 src/Supabase/AuthHelpers.js+2−1 src/Supabase/AuthHelpers.purs+4−2 src/Supabase/AuthHelpers/React.js+3 src/Supabase/AuthHelpers/React.purs+22 src/Supabase/Realtime.js+5 src/Supabase/Realtime.purs+59 src/Supabase/Realtime/ListenType.purs+18 src/Supabase/Realtime/SubscribeStates.purs+25 src/Supabase/Supabase.js+6 src/Supabase/Supabase.purs+40−10 src/Supabase/Types.purs+5−3 test/Main.purs+3
spago.dhall+4
@@ -6,6 +6,7 @@
66 , "datetime"
77 , "effect"
88 , "either"
9+ , "exceptions"
910 , "fetch"
1011 , "fetch-core"
1112 , "foldable-traversable"
@@ -21,6 +22,9 @@
2122 , "transformers"
2223 , "tuples"
2324 , "typelevel-prelude"
25+ , "unsafe-coerce"
26+ , "unsafe-reference"
27+ , "untagged-union"
2428 , "web-file"
2529 , "yoga-json"
2630 ]
src/Supabase/AuthHelpers.js+2−1
@@ -1,8 +1,9 @@
1+'use client'
12import { useUser } from "@supabase/auth-helpers-react";
3+import { createBrowserSupabaseClient } from "@supabase/auth-helpers-nextjs";
24
35export const useUserImpl = useUser;
46
5−import { createBrowserSupabaseClient } from "@supabase/auth-helpers-nextjs";
67
78export const createBrowserClient = () => createBrowserSupabaseClient();
89
src/Supabase/AuthHelpers.purs+4−2
@@ -20,6 +20,7 @@ import Record.Studio (mapRecordKind)
2020import Supabase.Supabase as Supabase
2121import Supabase.Types (Client)
2222import Yoga.JSON as YogaJSON
23+import Yoga.JSON (class WriteForeign)
2324
2425foreign import useUserImpl :: Effect { user :: Nullable Supabase.User }
2526
@@ -43,14 +44,15 @@ type Options =
4344 { db :: Maybe String
4445 }
4546
46−type ClientOptions =
47+type ClientOptions r =
4748 { cookieOptions :: Maybe CookieOptions
4849 , options :: Maybe Options
4950 , supabaseKey :: Maybe String
5051 , supabaseUrl :: Maybe String
52+ | r
5153 }
5254
5355foreign import createBrowserClientWithOptionsInternal :: Foreign -> Effect Client
5456
55−createBrowserClientWithOptions ∷ ClientOptions → Effect Client
57+createBrowserClientWithOptions ∷ forall r. WriteForeign (ClientOptions r) => ClientOptions r → Effect Client
5658createBrowserClientWithOptions = YogaJSON.write >>> createBrowserClientWithOptionsInternal
src/Supabase/AuthHelpers/React.jsnew file+3
@@ -0,0 +1,3 @@
1+import { useUser } from "@supabase/auth-helpers-react";
2+
3+export const useUserImpl = useUser;
src/Supabase/AuthHelpers/React.pursnew file+22
@@ -0,0 +1,22 @@
1+module Supabase.AuthHelpers.React where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe)
6+import Data.Nullable (Nullable)
7+import Data.Nullable as Nullable
8+import Effect (Effect)
9+import Foreign (Foreign)
10+import React.Basic.Hooks (Hook, unsafeHook)
11+import Record.Studio (mapRecordKind)
12+import Supabase.Supabase as Supabase
13+import Supabase.Types (Client)
14+import Yoga.JSON as YogaJSON
15+import Yoga.JSON (class WriteForeign)
16+
17+foreign import useUserImpl :: Effect { user :: Nullable Supabase.User }
18+
19+useUser :: Hook (UseUser) { user :: Maybe Supabase.User }
20+useUser = unsafeHook useUserImpl <#> mapRecordKind Nullable.toMaybe
21+
22+foreign import data UseUser :: Type -> Type
src/Supabase/Realtime.jsnew file+5
@@ -0,0 +1,5 @@
1+export const sendImpl = (what, channel) => channel.send(what)
2+
3+export const onImpl = (listenType, filter, callback, channel) => channel.on(listenType, filter, callback)
4+
5+export const subscribeImpl = (callback,timeout, channel) => channel.subscribe(callback,timeout)
src/Supabase/Realtime.pursnew file+59
@@ -0,0 +1,59 @@
1+module Supabase.Realtime where
2+
3+import Prelude
4+import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, mkEffectFn1, mkEffectFn2, runEffectFn2, runEffectFn3, runEffectFn4)
5+import Supabase.Types (Channel)
6+import Control.Promise (Promise, toAffE)
7+import Effect.Aff (Aff)
8+import Effect (Effect)
9+import Unsafe.Coerce (unsafeCoerce)
10+import Supabase.Realtime.ListenType (RealtimeListenType)
11+import Supabase.Realtime.SubscribeStates (RealtimeSubscribeState, RealtimeSubscribeState(ChannelError))
12+import Effect.Exception (Error, error)
13+import Untagged.Union (UndefinedOr, uorToMaybe)
14+import Data.Time.Duration (Milliseconds)
15+import Data.Maybe (Maybe, Maybe(Nothing), Maybe(Just))
16+import Supabase.Realtime.SubscribeStates (fromString) as SubscribeState
17+import Control.Monad.Error.Class (throwError)
18+
19+data RealtimeResponse = SendingOK | SendingTimedOut | SendingRateLimited
20+
21+foreign import sendImpl :: forall i. EffectFn2 { | i } Channel (Promise String)
22+
23+send :: forall i. { | i } -> Channel -> Aff RealtimeResponse
24+send input channel = do
25+ res <- runEffectFn2 sendImpl input channel # toAffE
26+ case res of
27+ "ok" -> pure SendingOK
28+ "timed out" -> pure SendingTimedOut
29+ "rate limited" -> pure SendingRateLimited
30+ _ -> throwError $ error $ "Supabase.Realtime.send: invalid response: " <> res
31+
32+foreign import onImpl :: forall f cbi. EffectFn4 RealtimeListenType f (EffectFn1 cbi Unit) Channel Unit
33+
34+on :: forall f cbi. RealtimeListenType -> f -> (cbi -> Effect Unit) -> Channel -> Effect Unit
35+on lt filter callback channel = runEffectFn4 onImpl lt filter (mkEffectFn1 callback) channel
36+
37+foreign import subscribeImpl
38+ :: EffectFn3
39+ (EffectFn2 String (UndefinedOr Error) Unit)
40+ Milliseconds
41+ Channel
42+ Unit
43+
44+subscribe
45+ :: Milliseconds
46+ -> (RealtimeSubscribeState -> Maybe Error -> Effect Unit)
47+ -> Channel
48+ -> Effect Unit
49+subscribe timeout cb channel =
50+ runEffectFn3 subscribeImpl
51+ ( mkEffectFn2 \st err ->
52+ case SubscribeState.fromString st of
53+ Nothing ->
54+ cb ChannelError (Just (error $ "Supabase.Realtime.subscribe: invalid subscribe state: " <> st))
55+ Just ok ->
56+ cb ok (uorToMaybe err)
57+ )
58+ timeout
59+ channel
src/Supabase/Realtime/ListenType.pursnew file+18
@@ -0,0 +1,18 @@
1+module Supabase.Realtime.ListenType where
2+
3+import Prelude
4+import Unsafe.Coerce (unsafeCoerce)
5+import Unsafe.Reference (unsafeRefEq)
6+
7+foreign import data RealtimeListenType :: Type
8+instance Eq RealtimeListenType where
9+ eq = unsafeRefEq
10+
11+broadcast :: RealtimeListenType
12+broadcast = unsafeCoerce "broadcast"
13+
14+presence :: RealtimeListenType
15+presence = unsafeCoerce "presence"
16+
17+postgresChanges :: RealtimeListenType
18+postgresChanges = unsafeCoerce "postgres_changes"
src/Supabase/Realtime/SubscribeStates.pursnew file+25
@@ -0,0 +1,25 @@
1+module Supabase.Realtime.SubscribeStates where
2+
3+import Prelude
4+import Unsafe.Coerce (unsafeCoerce)
5+import Unsafe.Reference (unsafeRefEq)
6+import Data.Maybe (Maybe(Nothing), Maybe, Maybe(Just))
7+
8+data RealtimeSubscribeState = Subscribed | TimedOut | Closed | ChannelError
9+derive instance Eq RealtimeSubscribeState
10+
11+toString :: RealtimeSubscribeState -> String
12+toString = case _ of
13+ Subscribed -> "SUBSCRIBED"
14+ TimedOut -> "TIMED_OUT"
15+ Closed -> "CLOSED"
16+ ChannelError -> "CHANNEL_ERROR"
17+
18+fromString :: String -> Maybe RealtimeSubscribeState
19+fromString = case _ of
20+ "SUBSCRIBED" -> Just Subscribed
21+ "TIMED_OUT" -> Just TimedOut
22+ "CLOSED" -> Just Closed
23+ "CHANNEL_ERROR" -> Just ChannelError
24+ _ -> Nothing
25+
src/Supabase/Supabase.js+6
@@ -8,6 +8,8 @@ export const getSessionImpl = (supabase) => () => supabase.auth.getSession();
88export const onAuthStateChangeImpl = (supabase) => (handler) => () =>
99 supabase.auth.onAuthStateChange((_event, session) => handler(session));
1010
11+export const getUserImpl = supabase => () => supabase.auth.getUser();
12+
1113export const signOutImpl = (supabase) => () => supabase.auth.signOut();
1214export const fromImpl = (client) => (table) => client.from(table);
1315export const selectQueryImpl = (queryBuilder) => (projection) =>
@@ -27,6 +29,8 @@ export const eqRunImpl = (builder) => (key, value) => () =>
2729
2830export const eqImpl = (builder) => (key, value) => builder.eq(key, value);
2931
32+export const orImpl = conditions => builder => builder.or(...conditions);
33+
3034export const singleImpl = (filterBuilder) => () => filterBuilder.single();
3135
3236export const maybeSingleImpl = (filterBuilder) => () =>
@@ -45,3 +49,5 @@ export const invokeImpl = (client) => (functionName, body, headers) => () =>
4549 body,
4650 headers,
4751 });
52+
53+export const channel = (channelName) => (client) => () => client.channel(channelName);
src/Supabase/Supabase.purs+40−10
@@ -14,18 +14,22 @@ module Supabase.Supabase
1414 , SessionData
1515 , SignInOptions
1616 , StatusR
17+ , class Or
1718 , User
1819 , class Equals
1920 , class Select
2021 , delete
2122 , equals
23+ , channel
2224 , from
2325 , getSession
2426 , invoke
2527 , maybeSingle
2628 , onAuthStateChange
29+ , or
2730 , range
2831 , select
32+ , getUser
2933 , signInWithOtp
3034 , signInWithOtpOptions
3135 , signOut
@@ -37,26 +41,28 @@ module Supabase.Supabase
3741import Prelude
3842
3943import Control.Promise (Promise)
40−import Control.Promise as Promise
44+import Data.Either (Either(..))
4145import Data.Function.Uncurried (Fn2, Fn3, runFn2, runFn3)
42−import Data.Maybe (Maybe)
46+import Data.Maybe (Maybe(..))
4347import Data.Nullable (Nullable)
44−import Data.Nullable as Nullable
4548import Data.Tuple (Tuple(..))
4649import Effect (Effect)
4750import Effect.Aff (Aff, Error)
51+import Effect.Exception (error)
4852import Effect.Uncurried (EffectFn1, EffectFn3, mkEffectFn1, runEffectFn3)
49−import Fetch as Fetch
50−import Fetch.Core.Response as YogaJson.Core
51−import Fetch.Internal.Response as FetchInternalResponse
5253import Foreign (Foreign)
5354import Prim.Row (class Union)
54−import Supabase.Types (Client)
55−import Supabase.Util as Util
55+import Supabase.Types (Channel, ChannelName, Client)
5656import Type.Function (type ($))
5757import Type.Row (type (+))
58−import Yoga.JSON (class ReadForeign, class WriteForeign, write) as YogaJson
5958import Yoga.JSON (class ReadForeign, class WriteForeign, writeImpl)
59+import Fetch as Fetch
60+import Fetch.Internal.Response as FetchInternalResponse
61+import Data.Nullable as Nullable
62+import Control.Promise as Promise
63+import Supabase.Util as Util
64+import Yoga.JSON (class ReadForeign, class WriteForeign, write) as YogaJson
65+import Fetch.Core.Response as YogaJson.Core
6066
6167foreign import data QueryBuilder :: Type
6268foreign import data FilterBuilder :: Type
@@ -123,6 +129,18 @@ else instance (YogaJson.ReadForeign output) => Equals FilterBuilder (Aff output)
123129 equals :: String -> String -> FilterBuilder -> Aff output
124130 equals l r builder = runFn2 (eqRunImpl builder) l r # Promise.toAffE >>= Util.fromJSON
125131
132+foreign import orImpl :: forall builderIn builderOut. Array String -> builderIn -> builderOut
133+
134+class Or builder output | output -> builder where
135+ or :: Array String -> builder -> output
136+
137+instance Or FilterBuilder FilterBuilder where
138+ or :: Array String -> FilterBuilder -> FilterBuilder
139+ or keys fb = orImpl keys fb
140+else instance Or QueryBuilder FilterBuilder where
141+ or :: Array String -> QueryBuilder -> FilterBuilder
142+ or keys fb = orImpl keys fb
143+
126144foreign import singleImpl :: FilterBuilder -> Effect (Promise Foreign)
127145
128146single :: forall t. YogaJson.ReadForeign t => FilterBuilder -> Aff (Response t)
@@ -169,7 +187,7 @@ foreign import onAuthStateChangeImpl :: Client -> (EffectFn1 (Nullable Session)
169187onAuthStateChange ∷ Client → (Maybe Session → Effect Unit) → Effect { data :: { id :: String, unsubscribe :: Effect Unit } }
170188onAuthStateChange client handler = onAuthStateChangeImpl client $ mkEffectFn1 (Nullable.toMaybe >>> handler)
171189
172−type User = { id :: String, email :: String }
190+type User = { id :: String, email :: String, user_metadata :: Foreign }
173191type Session = { user :: User }
174192type SessionData = { session :: Maybe Session }
175193
@@ -200,3 +218,15 @@ invoke client fn body headers = runFn3 (invokeImpl client) fn body headers # Pro
200218 where
201219 convertError { message, context } = { message, context: FetchInternalResponse.convert context }
202220 convert { "data": d, error } = { "data": Nullable.toMaybe d, error: Nullable.toMaybe error <#> convertError }
221+
222+foreign import channel :: ChannelName -> Client -> Effect Channel
223+
224+foreign import getUserImpl :: Client -> Effect (Promise { data :: Nullable User, error :: Nullable Error })
225+
226+getUser :: Client -> Aff (Either Error User)
227+getUser client = do
228+ res <- getUserImpl client # Promise.toAffE
229+ case Nullable.toMaybe res.data, Nullable.toMaybe res.error of
230+ Just user, _ -> pure $ Right user
231+ _, Just error -> pure $ Left error
232+ _, _ -> pure $ Left (error "Unexpected response error")
src/Supabase/Types.purs+5−3
@@ -1,7 +1,9 @@
1−module Supabase.Types
2− ( Client
3− ) where
1+module Supabase.Types where
42
53import Prelude
64
75foreign import data Client :: Type
6+
7+foreign import data Channel :: Type
8+
9+newtype ChannelName = ChannelName String
test/Main.purs+3
@@ -4,6 +4,9 @@ import Prelude
44
55import Effect (Effect)
66import Effect.Class.Console (log)
7+import Supabase (Client)
8+import Supabase.Supabase as SB
9+import Unsafe.Coerce (unsafeCoerce)
710
811main :: Effect Unit
912main = do