git.iko.soy/rowtype-yoga/purescript-supabase
on main help sign in
Initial commit
21ffb1esigma-andex
.gitignorenew file+10
@@ -0,0 +1,10 @@
1+/bower_components/
2+/node_modules/
3+/.pulp-cache/
4+/output/
5+/generated-docs/
6+/.psc-package/
7+/.psc*
8+/.purs*
9+/.psa*
10+/.spago
LICENSEnew file+16
@@ -0,0 +1,16 @@
1+MIT No Attribution
2+
3+Copyright 2021 Betalyra Sociedade Unipessoal Lda
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy of this
6+software and associated documentation files (the "Software"), to deal in the Software
7+without restriction, including without limitation the rights to use, copy, modify,
8+merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
9+permit persons to whom the Software is furnished to do so.
10+
11+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
12+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
13+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
14+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
15+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
16+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
README.md+2
@@ -1 +1,3 @@
1# purescript-supabase
1# purescript-supabase
2+
3+Puresript bindings for [supabase](https://supabase.com/).
package.jsonnew file+12
@@ -0,0 +1,12 @@
1+{
2+ "name": "purescript-supabase",
3+ "version": "1.0.0",
4+ "description": "",
5+ "main": "index.js",
6+ "scripts": {
7+ "test": "echo \"Error: no test specified\" && exit 1"
8+ },
9+ "keywords": [],
10+ "author": "",
11+ "license": "MIT-0"
12+}
packages.dhallnew file+105
@@ -0,0 +1,105 @@
1+{-
2+Welcome to your new Dhall package-set!
3+
4+Below are instructions for how to edit this file for most use
5+cases, so that you don't need to know Dhall to use it.
6+
7+## Use Cases
8+
9+Most will want to do one or both of these options:
10+1. Override/Patch a package's dependency
11+2. Add a package not already in the default package set
12+
13+This file will continue to work whether you use one or both options.
14+Instructions for each option are explained below.
15+
16+### Overriding/Patching a package
17+
18+Purpose:
19+- Change a package's dependency to a newer/older release than the
20+ default package set's release
21+- Use your own modified version of some dependency that may
22+ include new API, changed API, removed API by
23+ using your custom git repo of the library rather than
24+ the package set's repo
25+
26+Syntax:
27+where `entityName` is one of the following:
28+- dependencies
29+- repo
30+- version
31+-------------------------------
32+let upstream = --
33+in upstream
34+ with packageName.entityName = "new value"
35+-------------------------------
36+
37+Example:
38+-------------------------------
39+let upstream = --
40+in upstream
41+ with halogen.version = "master"
42+ with halogen.repo = "https://example.com/path/to/git/repo.git"
43+
44+ with halogen-vdom.version = "v4.0.0"
45+ with halogen-vdom.dependencies = [ "extra-dependency" ] # halogen-vdom.dependencies
46+-------------------------------
47+
48+### Additions
49+
50+Purpose:
51+- Add packages that aren't already included in the default package set
52+
53+Syntax:
54+where `<version>` is:
55+- a tag (i.e. "v4.0.0")
56+- a branch (i.e. "master")
57+- commit hash (i.e. "701f3e44aafb1a6459281714858fadf2c4c2a977")
58+-------------------------------
59+let upstream = --
60+in upstream
61+ with new-package-name =
62+ { dependencies =
63+ [ "dependency1"
64+ , "dependency2"
65+ ]
66+ , repo =
67+ "https://example.com/path/to/git/repo.git"
68+ , version =
69+ "<version>"
70+ }
71+-------------------------------
72+
73+Example:
74+-------------------------------
75+let upstream = --
76+in upstream
77+ with benchotron =
78+ { dependencies =
79+ [ "arrays"
80+ , "exists"
81+ , "profunctor"
82+ , "strings"
83+ , "quickcheck"
84+ , "lcg"
85+ , "transformers"
86+ , "foldable-traversable"
87+ , "exceptions"
88+ , "node-fs"
89+ , "node-buffer"
90+ , "node-readline"
91+ , "datetime"
92+ , "now"
93+ ]
94+ , repo =
95+ "https://github.com/hdgarrood/purescript-benchotron.git"
96+ , version =
97+ "v7.0.0"
98+ }
99+-------------------------------
100+-}
101+let upstream =
102+ https://github.com/purescript/package-sets/releases/download/psc-0.15.7-20230127/packages.dhall
103+ sha256:e0063f83308aa72cffe25444fe86fb484341496c099e38b64574cc7440768fdd
104+
105+in upstream
spago.dhallnew file+28
@@ -0,0 +1,28 @@
1+{ name = "supabase"
2+, dependencies =
3+ [ "aff"
4+ , "aff-promise"
5+ , "console"
6+ , "effect"
7+ , "either"
8+ , "fetch"
9+ , "fetch-core"
10+ , "foldable-traversable"
11+ , "foreign"
12+ , "functions"
13+ , "lists"
14+ , "maybe"
15+ , "nullable"
16+ , "prelude"
17+ , "react-basic-hooks"
18+ , "record"
19+ , "record-studio"
20+ , "transformers"
21+ , "tuples"
22+ , "typelevel-prelude"
23+ , "web-file"
24+ , "yoga-json"
25+ ]
26+, packages = ./packages.dhall
27+, sources = [ "src/**/*.purs", "test/**/*.purs" ]
28+}
src/Supabase/AuthHelpers.jsnew file+8
@@ -0,0 +1,8 @@
1+export {
2+ UserProvider as userProvider,
3+ useUser as useUserImpl,
4+} from "@supabase/auth-helpers-react";
5+
6+import { createBrowserSupabaseClient } from "@supabase/auth-helpers-nextjs";
7+
8+export const createBrowserClient = () => createBrowserSupabaseClient();
src/Supabase/AuthHelpers.pursnew file+29
@@ -0,0 +1,29 @@
1+module Supabase.AuthHelpers
2+ ( UseUser
3+ , createBrowserClient
4+ , useUser
5+ , userProvider
6+ )
7+ where
8+
9+import Prelude
10+
11+import Data.Maybe (Maybe)
12+import Data.Nullable (Nullable)
13+import Data.Nullable as Nullable
14+import Effect (Effect)
15+import React.Basic.Hooks (Hook, ReactComponent, unsafeHook)
16+import Record.Studio (mapRecordKind)
17+import Supabase.Supabase as Supabase
18+import Supabase.Types (Client)
19+
20+foreign import userProvider :: forall props. ReactComponent { supabaseClient :: Client | props }
21+
22+foreign import useUserImpl :: Effect { user :: Nullable Supabase.User }
23+
24+useUser :: Hook (UseUser) { user :: Maybe Supabase.User }
25+useUser = unsafeHook useUserImpl <#> mapRecordKind Nullable.toMaybe
26+
27+foreign import data UseUser :: Type -> Type
28+
29+foreign import createBrowserClient :: Effect Client
src/Supabase/Storage.jsnew file+15
@@ -0,0 +1,15 @@
1+import { supabaseClient } from "@supabase/auth-helpers-nextjs";
2+
3+export const storage = (client) => client.storage;
4+
5+export const fromImpl = (storage) => (table) => storage.from(table);
6+
7+export const uploadImpl = (queryBuilder) => (filePath, file, fileOptions) => () =>
8+ queryBuilder.upload(filePath, file, fileOptions);
9+
10+export const downloadImpl = (queryBuilder) => (filePath) => () =>
11+ queryBuilder.download(filePath);
12+
13+
14+export const removeImpl = (queryBuilder) => (filePaths) => () =>
15+ queryBuilder.remove(filePaths);
src/Supabase/Storage.pursnew file+59
@@ -0,0 +1,59 @@
1+module Supabase.Storage
2+ ( FileOptions
3+ , Storage
4+ , download
5+ , from
6+ , remove
7+ , storage
8+ , upload
9+ ) where
10+
11+import Prelude
12+
13+import Control.Promise (Promise)
14+import Control.Promise as Promise
15+import Data.Function.Uncurried (Fn3, runFn3)
16+import Data.Maybe (Maybe(..))
17+import Data.Nullable (Nullable)
18+import Data.Nullable as Nullable
19+import Effect (Effect)
20+import Effect.Aff (Aff)
21+import Foreign (Foreign)
22+import Supabase.Supabase (QueryBuilder, Response, ResultError)
23+import Supabase.Supabase as SBTypes
24+import Supabase.Types (Client)
25+import Type.Function (type ($))
26+import Supabase.Util as Util
27+import Web.File.Blob (Blob)
28+import Web.File.File as File
29+
30+foreign import data Storage :: Type
31+
32+foreign import storage :: Client -> Storage
33+foreign import fromImpl :: Storage -> String -> QueryBuilder
34+
35+from :: String -> Storage -> QueryBuilder
36+from = flip fromImpl
37+
38+type FileOptions = { upsert :: Boolean }
39+
40+foreign import uploadImpl :: QueryBuilder -> Fn3 String File.File FileOptions $ Effect (Promise Foreign)
41+
42+upload ∷ String → File.File → FileOptions -> QueryBuilder → Aff (Response { path :: String })
43+upload filePath file fileOptions qb = runFn3 (uploadImpl qb) filePath file fileOptions # Promise.toAffE >>= Util.fromJSON
44+
45+foreign import downloadImpl :: QueryBuilder -> String -> Effect (Promise { "data" :: Nullable Blob, error :: Nullable ResultError })
46+
47+download :: String -> QueryBuilder -> Aff (SBTypes.Response Blob) -- [TODO] Make this a proper response
48+download file qb = downloadImpl qb file # Promise.toAffE <#> convert
49+ where
50+ convert { "data": d, error: error } =
51+ let
52+ blob = Nullable.toMaybe d
53+ in
54+ { "data": blob, error: Nullable.toMaybe error, status: Nothing }
55+
56+foreign import removeImpl :: QueryBuilder -> Array String -> Effect (Promise Foreign)
57+
58+remove ∷ Array String → QueryBuilder → Aff { error ∷ Maybe ResultError }
59+remove file qb = removeImpl qb file # Promise.toAffE >>= Util.fromJSON
src/Supabase/Supabase.jsnew file+49
@@ -0,0 +1,49 @@
1+import { supabaseClient } from "@supabase/auth-helpers-nextjs";
2+
3+
4+// supabaseClient.from("bla")
5+// .select("wurst")
6+// .eq("", 1)
7+// .single()
8+
9+export const signInWithOtpImpl = (supabase) => (email) => () =>
10+ supabase.auth.signInWithOtp({ email });
11+
12+export const getSessionImpl = (supabase) => () => supabase.auth.getSession();
13+export const onAuthStateChangeImpl = (supabase) => (handler) => () =>
14+ supabase.auth.onAuthStateChange((_event, session) => handler(session));
15+
16+export const signOutImpl = (supabase) => () => supabase.auth.signOut();
17+export const fromImpl = (client) => (table) => client.from(table);
18+export const selectQueryImpl = (queryBuilder) => (projection) =>
19+ queryBuilder.select(projection);
20+
21+export const selectQueryWithCountImpl =
22+ (queryBuilder) => (projection) => (count) =>
23+ queryBuilder.select(projection, { count });
24+
25+export const deleteImpl = (queryBuilder) => queryBuilder.delete();
26+
27+export const updateImpl = (queryBuilder) => (record) =>
28+ queryBuilder.update(record);
29+
30+export const eqRunImpl = (builder) => (key, value) => () =>
31+ builder.eq(key, value);
32+
33+export const eqImpl = (builder) => (key, value) => builder.eq(key, value);
34+
35+export const singleImpl = (filterBuilder) => () => filterBuilder.single();
36+
37+export const rangeImpl = (from) => (to) => (filterBuilder) => () =>
38+ filterBuilder.range(from, to);
39+export const upsertImpl = (queryBuilder) => (values) =>
40+ queryBuilder.upsert(values);
41+
42+export const selectRunImpl = (queryBuilder) => (input) => () =>
43+ queryBuilder.select(input);
44+
45+export const invokeImpl = (client) => (functionName, body, headers) => () =>
46+ client.functions.invoke(functionName, {
47+ body,
48+ headers,
49+ });
src/Supabase/Supabase.pursnew file+183
@@ -0,0 +1,183 @@
1+module Supabase.Supabase
2+ ( AuthResponse
3+ , Count(..)
4+ , CountR
5+ , CountResponse
6+ , DataR
7+ , ErrorR
8+ , FilterBuilder
9+ , FunctionResponse
10+ , QueryBuilder
11+ , Response
12+ , ResultError
13+ , Session
14+ , SessionData
15+ , StatusR
16+ , User
17+ , class Equals
18+ , class Select
19+ , delete
20+ , equals
21+ , from
22+ , getSession
23+ , invoke
24+ , onAuthStateChange
25+ , range
26+ , select
27+ , signInWithOtp
28+ , signOut
29+ , single
30+ , update
31+ , upsert
32+ )
33+ where
34+
35+import Prelude
36+
37+import Control.Promise (Promise)
38+import Control.Promise as Promise
39+import Data.Function.Uncurried (Fn2, Fn3, runFn2, runFn3)
40+import Data.Maybe (Maybe)
41+import Data.Nullable (Nullable)
42+import Data.Nullable as Nullable
43+import Data.Tuple (Tuple(..))
44+import Effect (Effect)
45+import Effect.Aff (Aff, Error)
46+import Effect.Uncurried (EffectFn1, mkEffectFn1)
47+import Fetch as Fetch
48+import Fetch.Core.Response as YogaJson.Core
49+import Fetch.Internal.Response as FetchInternalResponse
50+import Foreign (Foreign)
51+import Supabase.Types (Client)
52+import Type.Function (type ($))
53+import Type.Row (type (+))
54+import Supabase.Util as Util
55+import Yoga.JSON (class ReadForeign, class WriteForeign, write) as YogaJson
56+import Yoga.JSON (class ReadForeign, class WriteForeign, writeImpl)
57+
58+foreign import data QueryBuilder :: Type
59+foreign import data FilterBuilder :: Type
60+
61+data Count = Exact | Planned | Estimated
62+
63+countToString ∷ Count → String
64+countToString Exact = "exact"
65+countToString Planned = "planned"
66+countToString Estimated = "estimated"
67+
68+foreign import deleteImpl :: QueryBuilder -> FilterBuilder
69+
70+delete ∷ QueryBuilder -> FilterBuilder
71+delete = deleteImpl
72+
73+foreign import updateImpl :: QueryBuilder -> Foreign -> FilterBuilder
74+
75+update ∷ forall d. YogaJson.WriteForeign d => d -> QueryBuilder -> FilterBuilder
76+update d builder = updateImpl builder $ YogaJson.write d
77+
78+foreign import fromImpl :: Client -> String -> QueryBuilder
79+
80+from ∷ String → Client -> QueryBuilder
81+from s c = fromImpl c s
82+
83+foreign import upsertImpl :: QueryBuilder -> Foreign -> FilterBuilder
84+
85+upsert ∷ ∀ (values ∷ Type). WriteForeign values => values → QueryBuilder → FilterBuilder
86+upsert v q = upsertImpl q $ writeImpl v
87+
88+foreign import selectRunImpl :: forall input. FilterBuilder -> input -> Effect (Promise Foreign)
89+
90+foreign import selectQueryImpl :: QueryBuilder -> String -> FilterBuilder
91+
92+foreign import selectQueryWithCountImpl :: QueryBuilder -> String -> String -> FilterBuilder
93+
94+class Select input builder output where
95+ select :: input -> builder -> output
96+
97+instance (YogaJson.ReadForeign output) => Select Unit FilterBuilder (Aff output) where
98+ select input builder = selectRunImpl builder (input) # Promise.toAffE >>= Util.fromJSON
99+else instance (YogaJson.ReadForeign output) => Select String FilterBuilder (Aff output) where
100+ select input builder = selectRunImpl builder input # Promise.toAffE >>= Util.fromJSON
101+else instance Select (Tuple String Count) QueryBuilder FilterBuilder where
102+ select (Tuple input count) builder = selectQueryWithCountImpl builder input $ countToString count
103+else instance Select String QueryBuilder FilterBuilder where
104+ select = flip selectQueryImpl
105+
106+foreign import eqRunImpl :: FilterBuilder -> Fn2 String String $ Effect (Promise Foreign)
107+
108+foreign import eqImpl :: forall builderIn builderOut. builderIn -> Fn2 String String builderOut
109+
110+class Equals builder output | output -> builder where
111+ equals :: String -> String -> builder -> output
112+
113+instance Equals FilterBuilder FilterBuilder where
114+ equals :: String -> String -> FilterBuilder -> FilterBuilder
115+ equals k v fb = runFn2 (eqImpl fb) k v
116+else instance Equals QueryBuilder FilterBuilder where
117+ equals :: String -> String -> QueryBuilder -> FilterBuilder
118+ equals k v fb = runFn2 (eqImpl fb) k v
119+else instance (YogaJson.ReadForeign output) => Equals FilterBuilder (Aff output) where
120+ equals :: String -> String -> FilterBuilder -> Aff output
121+ equals l r builder = runFn2 (eqRunImpl builder) l r # Promise.toAffE >>= Util.fromJSON
122+
123+foreign import singleImpl :: FilterBuilder -> Effect (Promise Foreign)
124+
125+--type ResultResponse d = { "data" :: Maybe d, status :: Int, error :: Maybe ResultError }
126+
127+single :: forall t. YogaJson.ReadForeign t => FilterBuilder -> Aff (Response t)
128+single = singleImpl >>> Promise.toAffE >=> Util.fromJSON
129+
130+foreign import rangeImpl :: forall i. i -> i -> FilterBuilder -> Effect (Promise Foreign)
131+
132+type CountResponse t = { | DataR t + ErrorR + CountR + StatusR + () }
133+
134+range :: forall response. YogaJson.ReadForeign response => { from :: Int, to :: Int } -> FilterBuilder -> Aff response
135+range { from: f, to } = rangeImpl f to >>> Promise.toAffE >=> Util.fromJSON
136+
137+type InternalAuthResponse = { error :: Nullable Error }
138+
139+foreign import signInWithOtpImpl :: Client -> String -> Effect (Promise InternalAuthResponse)
140+
141+type AuthResponse = { error :: Maybe Error }
142+
143+signInWithOtp :: Client -> String -> Aff AuthResponse
144+signInWithOtp client email = signInWithOtpImpl client email # Promise.toAffE <#> \{ error } -> { error: Nullable.toMaybe error }
145+
146+foreign import signOutImpl :: Client -> Effect (Promise Unit)
147+
148+signOut ∷ Client → Aff Unit
149+signOut client = signOutImpl client # Promise.toAffE
150+
151+foreign import onAuthStateChangeImpl :: Client -> (EffectFn1 (Nullable Session) Unit) -> Effect Unit
152+
153+onAuthStateChange ∷ Client → (Maybe Session → Effect Unit) → Effect Unit
154+onAuthStateChange client handler = onAuthStateChangeImpl client $ mkEffectFn1 (Nullable.toMaybe >>> handler)
155+
156+type User = { id :: String, email :: String }
157+type Session = { user :: User }
158+type SessionData = { session :: Maybe Session }
159+
160+type ResultError = { code :: Maybe String, details :: Maybe String, message :: String }
161+
162+type DataR d r = (data :: Maybe d | r)
163+type ErrorR r = (error ∷ Maybe ResultError | r)
164+type CountR r = (count :: Int | r)
165+type StatusR r = (status ∷ Maybe Int)
166+type Response d = { data :: Maybe d, error ∷ Maybe ResultError, status ∷ Maybe Int }
167+
168+foreign import getSessionImpl :: Client -> Effect (Promise Foreign)
169+
170+getSession :: Client -> Aff (Response SessionData)
171+getSession client = getSessionImpl client # Promise.toAffE >>= Util.fromJSON
172+
173+type InternalFunctionResponse d = { "data" :: Nullable d, error :: Nullable { message :: String, context :: YogaJson.Core.Response}}
174+foreign import invokeImpl :: forall body headers t. Client -> Fn3 String body headers (Effect $ Promise $ InternalFunctionResponse t)
175+
176+type FunctionResponse d = { "data" :: Maybe d, error :: Maybe { message :: String, context :: Fetch.Response}}
177+
178+
179+invoke ∷ forall t body headers. ReadForeign t ⇒ Client → String → body → headers → Aff (FunctionResponse t)
180+invoke client fn body headers = runFn3 (invokeImpl client) fn body headers # Promise.toAffE <#> convert
181+ where
182+ convertError { message, context } = { message, context: FetchInternalResponse.convert context}
183+ convert { "data": d, error } = { "data": Nullable.toMaybe d, error: Nullable.toMaybe error <#> convertError}
src/Supabase/Types.pursnew file+7
@@ -0,0 +1,7 @@
1+module Supabase.Types
2+ ( Client
3+ ) where
4+
5+import Prelude
6+
7+foreign import data Client :: Type
src/Supabase/Util.pursnew file+27
@@ -0,0 +1,27 @@
1+module Supabase.Util
2+ ( fromEither
3+ , fromJSON
4+ , toError
5+ ) where
6+
7+import Prelude
8+import Prim.Row (class Nub, class Union)
9+
10+import Control.Monad.Error.Class (class MonadThrow)
11+import Data.Either (Either, either)
12+import Data.List.NonEmpty (NonEmptyList)
13+import Data.Semigroup.Foldable (intercalateMap)
14+import Effect.Aff (Error, error, throwError)
15+import Foreign (Foreign, ForeignError)
16+import Record (merge)
17+import Yoga.JSON as Yoga
18+import Yoga.JSON.Error (renderHumanError)
19+
20+toError :: NonEmptyList ForeignError -> Error
21+toError = intercalateMap "\n" renderHumanError >>> error
22+
23+fromJSON :: forall json m. MonadThrow Error m => Yoga.ReadForeign json ⇒ Foreign -> m json
24+fromJSON = Yoga.read >>> either (toError >>> throwError) pure
25+
26+fromEither ∷ ∀ t m. MonadThrow Error m ⇒ Either Error t → m t
27+fromEither = either throwError pure
test/Main.pursnew file+11
@@ -0,0 +1,11 @@
1+module Test.Main where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Class.Console (log)
7+
8+main :: Effect Unit
9+main = do
10+ log "🍝"
11+ log "You should add some tests."