diff --git a/.gitmodules b/.gitmodules index f5c27d99..099b4b65 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "modules/functions-csharp"] path = modules/functions-csharp url = https://github.com/supabase-community/functions-csharp +[submodule "modules/core-csharp"] + path = modules/core-csharp + url = git@github.com:supabase-community/core-csharp.git diff --git a/README.md b/README.md index 762d4393..54b2019c 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@

-

Stage: Beta

- --- Integrate your [Supabase](https://supabase.io) projects with C#. @@ -42,40 +40,35 @@ public async void Main() var url = Environment.GetEnvironmentVariable("SUPABASE_URL"); var key = Environment.GetEnvironmentVariable("SUPABASE_KEY"); - await Supabase.Client.InitializeAsync(url, key); + var client = new Supabase.Client(url, key); + await client.InitializeAsync(); // That's it - forreal. Crazy right? - // The Supabase Instance can be accessed at any time using: - // Supabase.Client.Instance {.Realtime|.Auth|etc.} - // For ease of readability we'll use this: - var instance = Supabase.Client.Instance; - // Access Postgrest using: - var channels = await instance.From().Get(); + var channels = await client.From().Get(); // Access Auth using: - await instance.Auth.SignIn(email, password); - Debug.WriteLine(instance.Auth.CurrentUser.Id); + await client.Auth.SignIn(email, password); + Debug.WriteLine(client.Auth.CurrentUser.Id); // Interested in Realtime Events? - var table = await instance.From(); + var table = await client.From(); table.On(ChannelEventType.Insert, Channel_Inserted); table.On(ChannelEventType.Delete, Channel_Deleted); table.On(ChannelEventType.Update, Channel_Updated); // Invoke an Edge Function - var result = await instance.Functions.Invoke("hello", new Dictionary { + var result = await client.Functions.Invoke("hello", new Dictionary { { "name", "Ronald" } }); // Run a Remote Stored Procedure: - await instance.Rpc("my_cool_procedure", params); + await client.Rpc("my_cool_procedure", params); // Interact with Supabase Storage - var storage = Supabase.Client.Instance.Storage - await storage.CreateBucket("testing") + await client.Storage.CreateBucket("testing") - var bucket = storage.From("testing"); + var bucket = client.Storage.From("testing"); var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase).Replace("file:", ""); var imagePath = Path.Combine(basePath, "Assets", "supabase-csharp.png"); diff --git a/Supabase/Client.cs b/Supabase/Client.cs index ade0de8c..172f872d 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -1,20 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Postgrest; +using Postgrest.Interfaces; using Postgrest.Models; using Postgrest.Responses; +using Storage.Interfaces; +using Supabase.Core; +using Supabase.Functions.Interfaces; using Supabase.Gotrue; -using static Supabase.Functions.Client; +using Supabase.Gotrue.Interfaces; +using Supabase.Interfaces; +using Supabase.Realtime; +using Supabase.Realtime.Interfaces; +using Supabase.Storage; +using static Supabase.Gotrue.Constants; namespace Supabase { /// /// A singleton class representing a Supabase Client. /// - public class Client + public class Client : ISupabaseClient { public enum ChannelEventType { @@ -27,169 +33,206 @@ public enum ChannelEventType /// /// Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies. /// - public Gotrue.Client Auth { get; private set; } - public Realtime.Client Realtime { get; private set; } + public IGotrueClient Auth + { + get + { + return _auth; + } + set + { + // Remove existing internal state listener (if applicable) + if (_auth != null) + _auth.StateChanged -= Auth_StateChanged; + + _auth = value; + _auth.StateChanged += Auth_StateChanged; + } + } + private IGotrueClient _auth; /// - /// Supabase Edge functions allow you to deploy and invoke edge functions. + /// Supabase Realtime allows for realtime feedback on database changes. /// - public SupabaseFunctions Functions => new SupabaseFunctions(instance.FunctionsUrl, instance.GetAuthHeaders()); - - private Postgrest.Client Postgrest() => global::Postgrest.Client.Initialize(instance.RestUrl, new Postgrest.ClientOptions - { - Headers = instance.GetAuthHeaders(), - Schema = Schema - }); - - private static Client instance; - public static Client Instance + public IRealtimeClient Realtime { get { - if (instance == null) - { - Debug.WriteLine("Supabase must be initialized before it is called."); - return null; - } - return instance; + return _realtime; + } + set + { + // Disconnect from previous socket (if applicable) + if (_realtime != null) + _realtime.Disconnect(); + + _realtime = value; } } + private IRealtimeClient _realtime; - public string SupabaseKey { get; private set; } - public string SupabaseUrl { get; private set; } - public string AuthUrl { get; private set; } - public string RestUrl { get; private set; } - public string RealtimeUrl { get; private set; } - public string StorageUrl { get; private set; } - public string FunctionsUrl { get; private set; } - public string Schema { get; private set; } + /// + /// Supabase Edge functions allow you to deploy and invoke edge functions. + /// + public IFunctionsClient Functions + { + get => _functions; + set => _functions = value; + } + private IFunctionsClient _functions; - private SupabaseOptions options; + /// + /// Supabase Postgrest allows for strongly typed REST interactions with the your database. + /// + public IPostgrestClient Postgrest + { + get => _postgrest; + set => _postgrest = value; + } + private IPostgrestClient _postgrest; - private Client() { } + /// + /// Supabase Storage allows you to manage user-generated content, such as photos or videos. + /// + public IStorageClient Storage + { + get => _storage; + set => _storage = value; + } + private IStorageClient _storage; + private string? supabaseKey; + private SupabaseOptions options; /// - /// Initializes a Supabase Client. + /// Constructor supplied for dependency injection support. /// - /// - /// + /// + /// + /// + /// + /// /// - /// - public static void Initialize(string supabaseUrl, string supabaseKey, SupabaseOptions options = null, Action callback = null) + public Client(IGotrueClient auth, IRealtimeClient realtime, IFunctionsClient functions, IPostgrestClient postgrest, IStorageClient storage, SupabaseOptions options) { - Task.Run(async () => - { - var result = await InitializeAsync(supabaseUrl, supabaseKey, options); - callback?.Invoke(result); - }); + _auth = auth; + _realtime = realtime; + _functions = functions; + _postgrest = postgrest; + _storage = storage; + this.options = options; } /// - /// Initializes a Supabase Client Asynchronously. + /// Creates a new Supabase Client. /// /// /// /// - /// - public static async Task InitializeAsync(string supabaseUrl, string supabaseKey, SupabaseOptions options = null) + public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options = null) { - instance = new Client(); - - instance.SupabaseUrl = supabaseUrl; - instance.SupabaseKey = supabaseKey; + this.supabaseKey = supabaseKey; - if (options == null) - options = new SupabaseOptions(); + options ??= new SupabaseOptions(); + this.options = options; - instance.options = options; - instance.AuthUrl = string.Format(options.AuthUrlFormat, supabaseUrl); - instance.RestUrl = string.Format(options.RestUrlFormat, supabaseUrl); - instance.RealtimeUrl = string.Format(options.RealtimeUrlFormat, supabaseUrl).Replace("http", "ws"); - instance.StorageUrl = string.Format(options.StorageUrlFormat, supabaseUrl); - instance.Schema = options.Schema; + var authUrl = string.Format(options.AuthUrlFormat, supabaseUrl); + var restUrl = string.Format(options.RestUrlFormat, supabaseUrl); + var realtimeUrl = string.Format(options.RealtimeUrlFormat, supabaseUrl).Replace("http", "ws"); + var storageUrl = string.Format(options.StorageUrlFormat, supabaseUrl); + var schema = options.Schema; // See: https://github.com/supabase/supabase-js/blob/09065a65f171bc28a9fd7b831af2c24e5f1a380b/src/SupabaseClient.ts#L77-L83 var isPlatform = new Regex(@"(supabase\.co)|(supabase\.in)").Match(supabaseUrl); + string? functionsUrl; if (isPlatform.Success) { var parts = supabaseUrl.Split('.'); - instance.FunctionsUrl = $"{parts[0]}.functions.{parts[1]}.{parts[2]}"; + functionsUrl = $"{parts[0]}.functions.{parts[1]}.{parts[2]}"; } else { - instance.FunctionsUrl = string.Format(options.FunctionsUrlFormat, supabaseUrl); + functionsUrl = string.Format(options.FunctionsUrlFormat, supabaseUrl); } // Init Auth - instance.Auth = await Gotrue.Client.InitializeAsync(new Gotrue.ClientOptions + var gotrueOptions = new Gotrue.ClientOptions { - Url = instance.AuthUrl, - Headers = instance.GetAuthHeaders(), + Url = authUrl, AutoRefreshToken = options.AutoRefreshToken, PersistSession = options.PersistSession, - SessionDestroyer = options.SessionDestroyer, - SessionPersistor = options.SessionPersistor, - SessionRetriever = options.SessionRetriever - }); - instance.Auth.StateChanged += Auth_StateChanged; + SessionDestroyer = options.SessionHandler.SessionDestroyer, + SessionPersistor = options.SessionHandler.SessionPersistor, + SessionRetriever = options.SessionHandler.SessionRetriever + }; + + _auth = new Gotrue.Client(gotrueOptions); + _auth.StateChanged += Auth_StateChanged; + _auth.GetHeaders = () => GetAuthHeaders(); + // Init Realtime - if (options.ShouldInitializeRealtime) + + var realtimeOptions = new Realtime.ClientOptions { - instance.Realtime = Supabase.Realtime.Client.Initialize(instance.RealtimeUrl, new Realtime.ClientOptions - { - Parameters = { ApiKey = instance.SupabaseKey } - }); - - if (options.AutoConnectRealtime) - { - await instance.Realtime.ConnectAsync(); - } - } + Parameters = { ApiKey = this.supabaseKey } + }; - return instance; + _realtime = new Realtime.Client(realtimeUrl, realtimeOptions); + + _postgrest = new Postgrest.Client(restUrl, new Postgrest.ClientOptions { Schema = schema }); + _postgrest.GetHeaders = () => GetAuthHeaders(); + + _functions = new Functions.Client(functionsUrl); + _functions.GetHeaders = () => GetAuthHeaders(); + + _storage = new Storage.Client(storageUrl, GetAuthHeaders()); + _storage.GetHeaders = () => GetAuthHeaders(); } - private static void Auth_StateChanged(object sender, ClientStateChanged e) + + /// + /// Attempts to retrieve the session from Gotrue (set in ) and connects to realtime (if `options.AutoConnectRealtime` is set) + /// + public async Task> InitializeAsync() + { + await Auth.RetrieveSessionAsync(); + + if (options.AutoConnectRealtime) + { + await Realtime.ConnectAsync(); + } + return this; + } + + private void Auth_StateChanged(object sender, ClientStateChanged e) { switch (e.State) { // Pass new Auth down to Realtime // Ref: https://github.com/supabase-community/supabase-csharp/issues/12 - case Gotrue.Client.AuthState.SignedIn: - case Gotrue.Client.AuthState.TokenRefreshed: - if (Instance.Realtime != null) - { - Instance.Realtime.SetAuth(Instance.Auth.CurrentSession.AccessToken); - } + case AuthState.SignedIn: + case AuthState.TokenRefreshed: + if (Auth.CurrentSession?.AccessToken != null) + Realtime.SetAuth(Auth.CurrentSession.AccessToken); break; // Remove Realtime Subscriptions on Auth Signout. - case Gotrue.Client.AuthState.SignedOut: - if (Instance.Realtime != null) - { - foreach (var subscription in Instance.Realtime.Subscriptions.Values) - subscription.Unsubscribe(); - - Instance.Realtime.Disconnect(); - } + case AuthState.SignedOut: + foreach (var subscription in Realtime.Subscriptions.Values) + subscription.Unsubscribe(); + Realtime.Disconnect(); break; } } - /// - /// Supabase Storage allows you to manage user-generated content, such as photos or videos. - /// - public Storage.Client Storage => new Storage.Client(StorageUrl, GetAuthHeaders()); - /// /// Gets the Postgrest client to prepare for a query. /// - /// + /// /// - public SupabaseTable From() where T : BaseModel, new() => new SupabaseTable(); + public ISupabaseTable From() where TModel : BaseModel, new() => new SupabaseTable(Postgrest, Realtime); /// /// Runs a remote procedure. @@ -197,14 +240,18 @@ private static void Auth_StateChanged(object sender, ClientStateChanged e) /// /// /// - public Task Rpc(string procedureName, Dictionary parameters) => Postgrest().Rpc(procedureName, parameters); - + public Task Rpc(string procedureName, Dictionary parameters) => _postgrest.Rpc(procedureName, parameters); internal Dictionary GetAuthHeaders() { var headers = new Dictionary(); - headers["apiKey"] = SupabaseKey; - headers["X-Client-Info"] = Util.GetAssemblyVersion(); + + headers["X-Client-Info"] = Util.GetAssemblyVersion(typeof(Client)); + + if (supabaseKey != null) + { + headers["apiKey"] = supabaseKey; + } // In Regard To: https://github.com/supabase/supabase-csharp/issues/5 if (options.Headers.ContainsKey("Authorization")) @@ -213,63 +260,11 @@ internal Dictionary GetAuthHeaders() } else { - var bearer = Auth?.CurrentSession?.AccessToken != null ? Auth.CurrentSession.AccessToken : SupabaseKey; + var bearer = Auth.CurrentSession?.AccessToken != null ? Auth.CurrentSession.AccessToken : supabaseKey; headers["Authorization"] = $"Bearer {bearer}"; } return headers; } } - - /// - /// Options available for Supabase Client Configuration - /// - public class SupabaseOptions - { - public string Schema = "public"; - - /// - /// Should the Client automatically handle refreshing the User's Token? - /// - public bool AutoRefreshToken { get; set; } = true; - - /// - /// Should the Client Initialize Realtime? - /// - public bool ShouldInitializeRealtime { get; set; } = false; - - /// - /// Should the Client automatically connect to Realtime? - /// - public bool AutoConnectRealtime { get; set; } = false; - - /// - /// Should the Client call , , and ? - /// - public bool PersistSession { get; set; } = true; - - /// - /// Function called to persist the session (probably on a filesystem or cookie) - /// - public Func> SessionPersistor = (Session session) => Task.FromResult(true); - - /// - /// Function to retrieve a session (probably from the filesystem or cookie) - /// - public Func> SessionRetriever = () => Task.FromResult(null); - - /// - /// Function to destroy a session. - /// - public Func> SessionDestroyer = () => Task.FromResult(true); - - public Dictionary Headers = new Dictionary(); - - public string AuthUrlFormat { get; set; } = "{0}/auth/v1"; - public string RestUrlFormat { get; set; } = "{0}/rest/v1"; - public string RealtimeUrlFormat { get; set; } = "{0}/realtime/v1"; - public string StorageUrlFormat { get; set; } = "{0}/storage/v1"; - - public string FunctionsUrlFormat { get; set; } = "{0}/functions/v1"; - } } diff --git a/Supabase/DefaultSupabaseSessionHandler.cs b/Supabase/DefaultSupabaseSessionHandler.cs new file mode 100644 index 00000000..603373d3 --- /dev/null +++ b/Supabase/DefaultSupabaseSessionHandler.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Supabase.Gotrue; +using Supabase.Interfaces; + +namespace Supabase +{ + /// + /// Represents the default session handler for Gotrue - it does nothing by default. + /// + public class DefaultSupabaseSessionHandler : ISupabaseSessionHandler + { + public Task SessionPersistor(TSession session) where TSession : Session => Task.FromResult(true); + + + public Task SessionRetriever() where TSession : Session => Task.FromResult(null); + + + public Task SessionDestroyer() => Task.FromResult(true); + } +} diff --git a/Supabase/Interfaces/ISupabaseClient.cs b/Supabase/Interfaces/ISupabaseClient.cs new file mode 100644 index 00000000..2f345153 --- /dev/null +++ b/Supabase/Interfaces/ISupabaseClient.cs @@ -0,0 +1,34 @@ +using Postgrest.Interfaces; +using Postgrest.Models; +using Postgrest.Responses; +using Storage.Interfaces; +using Supabase.Functions.Interfaces; +using Supabase.Gotrue; +using Supabase.Gotrue.Interfaces; +using Supabase.Realtime; +using Supabase.Realtime.Interfaces; +using Supabase.Storage; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Supabase.Interfaces +{ + public interface ISupabaseClient + where TUser : User + where TSession : Session + where TSocket : IRealtimeSocket + where TChannel : IRealtimeChannel + where TBucket : Bucket + where TFileObject : FileObject + { + IGotrueClient Auth { get; set; } + IFunctionsClient Functions { get; set; } + IPostgrestClient Postgrest { get; set; } + IRealtimeClient Realtime { get; set; } + IStorageClient Storage { get; set; } + + ISupabaseTable From() where TModel : BaseModel, new(); + Task> InitializeAsync(); + Task Rpc(string procedureName, Dictionary parameters); + } +} \ No newline at end of file diff --git a/Supabase/Interfaces/ISupabaseFunctions.cs b/Supabase/Interfaces/ISupabaseFunctions.cs new file mode 100644 index 00000000..1d08c5fa --- /dev/null +++ b/Supabase/Interfaces/ISupabaseFunctions.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Supabase.Interfaces +{ + public interface ISupabaseFunctions + { + Task Invoke(string functionName, Dictionary? body = null); + Task Invoke(string functionName, Dictionary? body = null) where T : class; + Task RawInvoke(string functionName, Dictionary? body = null); + } +} \ No newline at end of file diff --git a/Supabase/Interfaces/ISupabaseSessionHandler.cs b/Supabase/Interfaces/ISupabaseSessionHandler.cs new file mode 100644 index 00000000..3761ccf7 --- /dev/null +++ b/Supabase/Interfaces/ISupabaseSessionHandler.cs @@ -0,0 +1,26 @@ +using Supabase.Gotrue; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Supabase.Interfaces +{ + public interface ISupabaseSessionHandler + { + /// + /// Function called to persist the session (probably on a filesystem or cookie) + /// + Task SessionPersistor(TSession session) where TSession : Session; + + /// + /// Function to retrieve a session (probably from the filesystem or cookie) + /// + Task SessionRetriever() where TSession : Session; + + /// + /// Function to destroy a session. + /// + Task SessionDestroyer(); + } +} diff --git a/Supabase/Interfaces/ISupabaseTable.cs b/Supabase/Interfaces/ISupabaseTable.cs new file mode 100644 index 00000000..882c41d9 --- /dev/null +++ b/Supabase/Interfaces/ISupabaseTable.cs @@ -0,0 +1,16 @@ +using Postgrest.Interfaces; +using Postgrest.Models; +using Supabase.Realtime; +using Supabase.Realtime.Interfaces; +using System; +using System.Threading.Tasks; + +namespace Supabase.Interfaces +{ + public interface ISupabaseTable : IPostgrestTable + where TModel : BaseModel, new() + where TChannel : IRealtimeChannel + { + Task On(Client.ChannelEventType e, Action action); + } +} \ No newline at end of file diff --git a/Supabase/StatelessClient.cs b/Supabase/StatelessClient.cs index 8130928f..42f002d8 100644 --- a/Supabase/StatelessClient.cs +++ b/Supabase/StatelessClient.cs @@ -6,8 +6,12 @@ using Postgrest; using Postgrest.Models; using Postgrest.Responses; +using Storage.Interfaces; +using Supabase.Core; using Supabase.Extensions; +using Supabase.Functions.Interfaces; using Supabase.Gotrue; +using Supabase.Storage; namespace Supabase { @@ -16,28 +20,29 @@ namespace Supabase /// public static class StatelessClient { - public static Gotrue.StatelessClient.StatelessClientOptions GetAuthOptions(string supabaseUrl, string supabaseKey = null, SupabaseOptions options = null) + public static Gotrue.ClientOptions GetAuthOptions(string supabaseUrl, string? supabaseKey = null, SupabaseOptions? options = null) + where TSession : Session { if (options == null) options = new SupabaseOptions(); var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); - return new Gotrue.StatelessClient.StatelessClientOptions + return new Gotrue.ClientOptions { Url = string.Format(options.AuthUrlFormat, supabaseUrl), Headers = headers }; } - public static Postgrest.StatelessClientOptions GetRestOptions(string supabaseUrl, string supabaseKey = null, SupabaseOptions options = null) + public static Postgrest.ClientOptions GetRestOptions(string? supabaseKey = null, SupabaseOptions? options = null) { if (options == null) options = new SupabaseOptions(); var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); - return new Postgrest.StatelessClientOptions(string.Format(options.RestUrlFormat, supabaseUrl)) + return new Postgrest.ClientOptions { Schema = options.Schema, Headers = headers @@ -51,7 +56,7 @@ public static Postgrest.StatelessClientOptions GetRestOptions(string supabaseUrl /// /// /// - public static Storage.Client Storage(string supabaseUrl, string supabaseKey = null, SupabaseOptions options = null) + public static IStorageClient Storage(string supabaseUrl, string? supabaseKey = null, SupabaseOptions? options = null) { if (options == null) options = new SupabaseOptions(); @@ -68,7 +73,7 @@ public static Storage.Client Storage(string supabaseUrl, string supabaseKey = nu /// /// /// - public static SupabaseFunctions Functions(string supabaseUrl, string supabaseKey, SupabaseOptions options = null) + public static IFunctionsClient Functions(string supabaseUrl, string supabaseKey, SupabaseOptions? options = null) { if (options == null) options = new SupabaseOptions(); @@ -88,8 +93,10 @@ public static SupabaseFunctions Functions(string supabaseUrl, string supabaseKey } var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); + var client = new Functions.Client(functionsUrl); + client.GetHeaders = () => headers; - return new SupabaseFunctions(functionsUrl, headers); + return client; } /// @@ -97,19 +104,23 @@ public static SupabaseFunctions Functions(string supabaseUrl, string supabaseKey /// /// /// - public static SupabaseTable From(string supabaseUrl, string supabaseKey, SupabaseOptions options = null) where T : BaseModel, new() + public static SupabaseTable From(string supabaseUrl, string supabaseKey, SupabaseOptions? options = null) where T : BaseModel, new() { if (options == null) options = new SupabaseOptions(); - var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); + var restUrl = string.Format(options.RestUrlFormat, supabaseUrl); + var realtimeUrl = string.Format(options.RealtimeUrlFormat, supabaseUrl).Replace("http", "ws"); + var restOptions = GetRestOptions(supabaseKey, options); + restOptions.Headers.MergeLeft(options.Headers); - return new SupabaseTable(string.Format(options.RestUrlFormat, supabaseUrl), new Postgrest.ClientOptions - { - Headers = headers, - Schema = options.Schema - }); + var realtimeOptions = new Realtime.ClientOptions { Parameters = { ApiKey = supabaseKey } }; + + var postgrestClient = new Postgrest.Client(restUrl, restOptions); + var realtimeClient = new Realtime.Client(realtimeUrl, realtimeOptions); + + return new SupabaseTable(postgrestClient, realtimeClient, options.Schema); } /// @@ -118,20 +129,25 @@ public static SupabaseFunctions Functions(string supabaseUrl, string supabaseKey /// /// /// - public static Task Rpc(string supabaseUrl, string supabaseKey, string procedureName, Dictionary parameters, SupabaseOptions options = null) + public static Task Rpc(string supabaseUrl, string supabaseKey, string procedureName, Dictionary parameters, SupabaseOptions? options = null) { if (options == null) options = new SupabaseOptions(); - return Postgrest.StatelessClient.Rpc(procedureName, parameters, GetRestOptions(supabaseUrl, supabaseKey, options)); + return new Postgrest.Client(string.Format(options.RestUrlFormat, supabaseUrl), GetRestOptions(supabaseKey, options)).Rpc(procedureName, parameters); } - internal static Dictionary GetAuthHeaders(string supabaseKey, SupabaseOptions options) + internal static Dictionary GetAuthHeaders(string? supabaseKey, SupabaseOptions options) { var headers = new Dictionary(); - headers["apiKey"] = supabaseKey; - headers["X-Client-Info"] = Util.GetAssemblyVersion(); + + headers["X-Client-Info"] = Util.GetAssemblyVersion(typeof(Client)); + + if (supabaseKey != null) + { + headers["apiKey"] = supabaseKey; + } // In Regard To: https://github.com/supabase/supabase-csharp/issues/5 if (options.Headers.ContainsKey("Authorization")) @@ -140,8 +156,7 @@ internal static Dictionary GetAuthHeaders(string supabaseKey, Su } else { - var bearer = supabaseKey; - headers["Authorization"] = $"Bearer {bearer}"; + headers["Authorization"] = $"Bearer {supabaseKey}"; } return headers; diff --git a/Supabase/Supabase.csproj b/Supabase/Supabase.csproj index 9229a430..a7996194 100644 --- a/Supabase/Supabase.csproj +++ b/Supabase/Supabase.csproj @@ -19,18 +19,24 @@ 0.5.3 + + enable + 8.0 + CS8600;CS8602;CS8603 + 0.5.3 $(VersionPrefix) - + - - - + + + - + + diff --git a/Supabase/SupabaseFunctions.cs b/Supabase/SupabaseFunctions.cs deleted file mode 100644 index 375319cf..00000000 --- a/Supabase/SupabaseFunctions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using static Supabase.Functions.Client; - -namespace Supabase -{ - public class SupabaseFunctions - { - private string _functionsUrl; - private Dictionary _headers = new Dictionary(); - - public SupabaseFunctions(string functionsUrl, Dictionary headers) - { - _functionsUrl = functionsUrl.TrimEnd('/'); - _headers = headers; - } - - public Task Invoke(string functionName, Dictionary body = null) => Functions.Client.Invoke($"{_functionsUrl}/{functionName}", options: new InvokeFunctionOptions - { - Headers = _headers, - Body = body - }); - - public Task Invoke(string functionName, Dictionary body = null) => Functions.Client.Invoke($"{_functionsUrl}/{functionName}", options: new InvokeFunctionOptions - { - Headers = _headers, - Body = body - }); - - public Task RawInvoke(string functionName, Dictionary body = null) => Functions.Client.RawInvoke($"{_functionsUrl}/{functionName}", options: new InvokeFunctionOptions - { - Headers = _headers, - Body = body - }); - } -} diff --git a/Supabase/SupabaseModel.cs b/Supabase/SupabaseModel.cs index 79e49880..b36c6c6b 100644 --- a/Supabase/SupabaseModel.cs +++ b/Supabase/SupabaseModel.cs @@ -1,21 +1,11 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Postgrest.Models; -using Postgrest.Responses; +using Postgrest.Models; +using System; +using System.Collections.Generic; +using System.Text; namespace Supabase { + [Obsolete] public abstract class SupabaseModel : BaseModel - { - public override Task> Update(CancellationToken cancellationToken = default(CancellationToken)) - { - return Client.Instance.From().Update(this as T); - } - - public override Task Delete(CancellationToken cancellationToken = default(CancellationToken)) - { - return Client.Instance.From().Delete(this as T); - } - } + {} } diff --git a/Supabase/SupabaseOptions.cs b/Supabase/SupabaseOptions.cs new file mode 100644 index 00000000..ccd1bc6d --- /dev/null +++ b/Supabase/SupabaseOptions.cs @@ -0,0 +1,49 @@ +using Supabase.Gotrue; +using Supabase.Interfaces; +using System; +using System.Collections.Generic; + +namespace Supabase +{ + /// + /// Options available for Supabase Client Configuration + /// + public class SupabaseOptions + { + /// + /// Schema to be used in Postgres / Realtime + /// + public string Schema = "public"; + + /// + /// Should the Client automatically handle refreshing the User's Token? + /// + public bool AutoRefreshToken { get; set; } = true; + + /// + /// Should the Client automatically connect to Realtime? + /// + public bool AutoConnectRealtime { get; set; } = false; + + /// + /// Should the Client call , , and ? + /// + public bool PersistSession { get; set; } = true; + + /// + /// Functions passed to Gotrue that handle sessions. + /// + /// **By default these do nothing for persistence.** + /// + public ISupabaseSessionHandler SessionHandler { get; set; } = new DefaultSupabaseSessionHandler(); + + public Dictionary Headers = new Dictionary(); + + public string AuthUrlFormat { get; set; } = "{0}/auth/v1"; + public string RestUrlFormat { get; set; } = "{0}/rest/v1"; + public string RealtimeUrlFormat { get; set; } = "{0}/realtime/v1"; + public string StorageUrlFormat { get; set; } = "{0}/storage/v1"; + + public string FunctionsUrlFormat { get; set; } = "{0}/functions/v1"; + } +} diff --git a/Supabase/SupabaseTable.cs b/Supabase/SupabaseTable.cs index a3e28ab4..c2acffe1 100644 --- a/Supabase/SupabaseTable.cs +++ b/Supabase/SupabaseTable.cs @@ -2,21 +2,32 @@ using System.Collections.Generic; using System.Threading.Tasks; using Postgrest; +using Postgrest.Interfaces; using Postgrest.Models; +using Supabase.Interfaces; using Supabase.Realtime; +using Supabase.Realtime.Interfaces; using static Supabase.Client; namespace Supabase { - public class SupabaseTable : Table where T : BaseModel, new() + public class SupabaseTable : Table, ISupabaseTable + where TModel : BaseModel, new() { - private Channel channel; + private Channel? channel; - public SupabaseTable() : base(Client.Instance.RestUrl, new Postgrest.ClientOptions { Headers = Instance.GetAuthHeaders(), Schema = Instance.Schema }) - { } + private IPostgrestClient postgrestClient; - public SupabaseTable(string restUrl, Postgrest.ClientOptions options) : base(restUrl, options) - { } + private IRealtimeClient realtimeClient; + + private string schema; + + public SupabaseTable(IPostgrestClient postgrestClient, IRealtimeClient realtimeClient, string schema = "public") : base(postgrestClient.BaseUrl, Postgrest.Client.SerializerSettings(postgrestClient.Options), postgrestClient.Options) + { + this.postgrestClient = postgrestClient; + this.realtimeClient = realtimeClient; + this.schema = schema; + } public async Task On(ChannelEventType e, Action action) { @@ -25,31 +36,31 @@ public async Task On(ChannelEventType e, Action(); // In regard to: https://github.com/supabase/supabase-js/pull/270 - var headers = Instance.GetAuthHeaders(); - if (headers.ContainsKey("Authorization")) + var headers = postgrestClient?.GetHeaders?.Invoke(); + if (headers != null && headers.ContainsKey("Authorization")) { parameters.Add("user_token", headers["Authorization"].Split(' ')[1]); } - channel = Instance.Realtime.Channel("realtime", Instance.Schema, TableName, parameters: parameters); + channel = realtimeClient.Channel("realtime", schema, TableName, parameters: parameters); } - if (Instance.Realtime.Socket == null || !Instance.Realtime.Socket.IsConnected) - await Instance.Realtime.ConnectAsync(); + if (realtimeClient.Socket == null || !realtimeClient.Socket.IsConnected) + await realtimeClient.ConnectAsync(); switch (e) { case ChannelEventType.Insert: - channel.OnInsert += (sender, args) => action.Invoke(sender, args); + channel.OnInsert += (sender, args) => action?.Invoke(sender, args); break; case ChannelEventType.Update: - channel.OnUpdate += (sender, args) => action.Invoke(sender, args); + channel.OnUpdate += (sender, args) => action?.Invoke(sender, args); break; case ChannelEventType.Delete: - channel.OnDelete += (sender, args) => action.Invoke(sender, args); + channel.OnDelete += (sender, args) => action?.Invoke(sender, args); break; case ChannelEventType.All: - channel.OnMessage += (sender, args) => action.Invoke(sender, args); + channel.OnMessage += (sender, args) => action?.Invoke(sender, args); break; } diff --git a/Supabase/Util.cs b/Supabase/Util.cs deleted file mode 100644 index 92fec65b..00000000 --- a/Supabase/Util.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Diagnostics; -using System.Reflection; -using System.Runtime.InteropServices; - -namespace Supabase -{ - public static class Util - { - public static string GetAssemblyVersion() - { - var assembly = typeof(Supabase.Client).Assembly; - var informationVersion = assembly.GetCustomAttribute().InformationalVersion; - var name = assembly.GetName().Name; - - return $"{name.ToString().ToLower()}-csharp/{informationVersion}"; - } - } -} diff --git a/SupabaseExample/Models/Channel.cs b/SupabaseExample/Models/Channel.cs index 34d386ad..15bdc35c 100644 --- a/SupabaseExample/Models/Channel.cs +++ b/SupabaseExample/Models/Channel.cs @@ -1,11 +1,12 @@ using System; using Postgrest.Attributes; +using Postgrest.Models; using Supabase; namespace SupabaseExample.Models { [Table("channels")] - public class Channel : SupabaseModel + public class Channel : BaseModel { [PrimaryKey("id", false)] // Key is Autogenerated public int Id { get; set; } diff --git a/SupabaseExample/Models/Movie.cs b/SupabaseExample/Models/Movie.cs index 9bbcabe5..e5f29ba5 100644 --- a/SupabaseExample/Models/Movie.cs +++ b/SupabaseExample/Models/Movie.cs @@ -8,7 +8,7 @@ namespace SupabaseExample.Models { [Table("movie")] - public class Movie : SupabaseModel + public class Movie : BaseModel { [PrimaryKey("id", false)] public int Id { get; set; } @@ -25,7 +25,7 @@ public class Movie : SupabaseModel } [Table("person")] - public class Person : SupabaseModel + public class Person : BaseModel { [PrimaryKey("id", false)] public int Id { get; set; } @@ -44,7 +44,7 @@ public class Person : SupabaseModel } [Table("profile")] - public class Profile : SupabaseModel + public class Profile : BaseModel { [Column("email")] public string Email { get; set; } diff --git a/SupabaseExample/Program.cs b/SupabaseExample/Program.cs index 886d2977..2f0c8ac0 100644 --- a/SupabaseExample/Program.cs +++ b/SupabaseExample/Program.cs @@ -14,18 +14,10 @@ static async Task Main(string[] args) var url = Environment.GetEnvironmentVariable("SUPABASE_URL"); var key = Environment.GetEnvironmentVariable("SUPABASE_KEY"); - await Supabase.Client.InitializeAsync(url, key, new Supabase.SupabaseOptions { AutoConnectRealtime = true, ShouldInitializeRealtime = true }); + var supabase = new Supabase.Client(url, key, new Supabase.SupabaseOptions { AutoConnectRealtime = true }); + await supabase.InitializeAsync(); - try - { - var instance = Supabase.Client.Instance; - } - catch (Exception ex) - { - // Handle exception here - } - - var reference = Supabase.Client.Instance.From(); + var reference = supabase.From(); await reference.On(Supabase.Client.ChannelEventType.All, (sender, ev) => { @@ -37,9 +29,9 @@ await reference.On(Supabase.Client.ChannelEventType.All, (sender, ev) => //await reference.Insert(new Models.Channel { Slug = GenerateName(10), InsertedAt = DateTime.Now }); #region Storage - var storage = Supabase.Client.Instance.Storage; + var storage = supabase.Storage; - var exists = (await storage.GetBucket("testing") != null); + var exists = await storage.GetBucket("testing") != null; if (!exists) await storage.CreateBucket("testing", new Supabase.Storage.BucketUpsertOptions { Public = true }); @@ -49,7 +41,7 @@ await reference.On(Supabase.Client.ChannelEventType.All, (sender, ev) => Debug.WriteLine($"[{b.Id}] {b.Name}"); var bucket = storage.From("testing"); - var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase).Replace("file:", ""); + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase).Replace("file:", "").Replace("C:\\", ""); var imagePath = Path.Combine(basePath, "Assets", "supabase-csharp.png"); Debug.WriteLine(await bucket.Upload(imagePath, "supabase-csharp.png", new Supabase.Storage.FileOptions { Upsert = true }, (sender, args) => Debug.WriteLine($"Upload Progress: {args}%"))); diff --git a/SupabaseTests/Client.cs b/SupabaseTests/Client.cs index c351cb7e..773ceee8 100644 --- a/SupabaseTests/Client.cs +++ b/SupabaseTests/Client.cs @@ -1,22 +1,19 @@ using System; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Supabase; using Supabase.Realtime; -using SupabaseTests.Models; -using static Supabase.Client; +using SupabaseTests.Stubs; namespace SupabaseTests { [TestClass] public class Client { - private string password = "I@M@SuperP@ssWord"; - private static Random random = new Random(); + private Supabase.Client Instance; + private static string RandomString(int length) { const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; @@ -27,14 +24,15 @@ private static string RandomString(int length) [TestInitialize] public async Task InitializeTest() { - await InitializeAsync("http://localhost", null, new Supabase.SupabaseOptions + + Instance = new Supabase.Client("http://localhost", null, new Supabase.SupabaseOptions { AuthUrlFormat = "{0}:9999", RealtimeUrlFormat = "{0}:4000/socket", RestUrlFormat = "{0}:3000", - ShouldInitializeRealtime = true, - AutoConnectRealtime = true + AutoConnectRealtime = true, }); + await Instance.InitializeAsync(); } [TestMethod("Client: Initializes.")] @@ -44,29 +42,27 @@ public void ClientInitializes() Assert.IsNotNull(Instance.Auth); } - //[TestMethod("Client: Connects to Realtime")] - //public async Task ClientConnectsToRealtime() - //{ - // var tsc = new TaskCompletionSource(); - - // var email = $"{RandomString(12)}@supabase.io"; - // await Instance.Auth.SignUp(email, password); + [TestMethod("Client: Connects to Realtime")] + public async Task ClientConnectsToRealtime() + { + var tsc = new TaskCompletionSource(); - // await Instance.Realtime.ConnectAsync(); + var email = $"{RandomString(12)}@supabase.io"; + await Instance.Auth.SignUp(email, RandomString(12)); - // var channel = Instance.Realtime.Channel("realtime", "public", "channels"); + var channel = Instance.Realtime.Channel("realtime", "public", "channels"); - // channel.StateChanged += (sender, ev) => - // { - // if (ev.State == Supabase.Realtime.Channel.ChannelState.Joined) - // tsc.SetResult(true); - // }; + channel.StateChanged += (sender, ev) => + { + if (ev.State == Channel.ChannelState.Joined) + tsc.SetResult(true); + }; - // await channel.Subscribe(); + await channel.Subscribe(); - // var result = await tsc.Task; - // Assert.IsTrue(result); - //} + var result = await tsc.Task; + Assert.IsTrue(result); + } [TestMethod("SupabaseModel: Successfully Updates")] public async Task SupabaseModelUpdates() @@ -98,5 +94,21 @@ public async Task SupabaseModelDeletes() Assert.AreEqual(0, result.Models.Count); } + + [TestMethod("Supports Dependency Injection for clients via property")] + public void SupportsDIForClientsViaProperty() + { + Instance.Auth = new FakeAuthClient(); + Instance.Functions = new FakeFunctionsClient(); + Instance.Realtime = new FakeRealtimeClient(); + Instance.Postgrest = new FakeRestClient(); + Instance.Storage = new FakeStorageClient(); + + Assert.ThrowsExceptionAsync(() => Instance.Auth.GetUser("")); + Assert.ThrowsExceptionAsync(() => Instance.Functions.Invoke("")); + Assert.ThrowsExceptionAsync(() => Instance.Realtime.ConnectAsync()); + Assert.ThrowsExceptionAsync(() => Instance.Postgrest.Rpc("", null)); + Assert.ThrowsExceptionAsync(() => Instance.Storage.ListBuckets()); + } } } diff --git a/SupabaseTests/Models/Channel.cs b/SupabaseTests/Models/Channel.cs index 19d5e710..dc40786b 100644 --- a/SupabaseTests/Models/Channel.cs +++ b/SupabaseTests/Models/Channel.cs @@ -1,11 +1,12 @@ using System; using Postgrest.Attributes; +using Postgrest.Models; using Supabase; namespace SupabaseTests.Models { [Table("channels")] - public class Channel : SupabaseModel + public class Channel : BaseModel { [PrimaryKey("id", false)] // Key is Autogenerated public int Id { get; set; } diff --git a/SupabaseTests/Models/Stub.cs b/SupabaseTests/Models/Stub.cs index 017355c7..3e6a5ed9 100644 --- a/SupabaseTests/Models/Stub.cs +++ b/SupabaseTests/Models/Stub.cs @@ -4,7 +4,7 @@ namespace SupabaseTests.Models { - public class Stub : SupabaseModel + public class Stub : BaseModel { } } diff --git a/SupabaseTests/Models/User.cs b/SupabaseTests/Models/User.cs index d706d70d..560dd694 100644 --- a/SupabaseTests/Models/User.cs +++ b/SupabaseTests/Models/User.cs @@ -7,7 +7,7 @@ namespace SupabaseTests.Models { [Table("users")] - public class User : SupabaseModel + public class User : BaseModel { [JsonProperty("username")] public string Username { get; set; } diff --git a/SupabaseTests/StatelessClient.cs b/SupabaseTests/StatelessClient.cs index 6a577238..f2cde036 100644 --- a/SupabaseTests/StatelessClient.cs +++ b/SupabaseTests/StatelessClient.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue; using SupabaseTests.Models; +using static Supabase.Gotrue.Constants; using static Supabase.StatelessClient; namespace SupabaseTests @@ -22,8 +24,8 @@ public class StatelessClient [TestMethod("Can access Stateless REST")] public async Task CanAccessStatelessRest() { - var restOptions = GetRestOptions(supabaseUrl, null, options); - var result1 = await Postgrest.StatelessClient.Table(restOptions).Get(); + var restOptions = GetRestOptions(null, options); + var result1 = await new Postgrest.Client(String.Format(options.RestUrlFormat, supabaseUrl), restOptions).Table().Get(); var result2 = await From(supabaseUrl, null, options).Get(); @@ -33,11 +35,11 @@ public async Task CanAccessStatelessRest() [TestMethod("Can access Stateless GoTrue")] public void CanAccessStatelessGotrue() { - var gotrueOptions = GetAuthOptions(supabaseUrl, null, options); + var gotrueOptions = GetAuthOptions(supabaseUrl, null, options); - Supabase.Gotrue.StatelessClient.GetApi(gotrueOptions).GetUser("my-user-jwt"); + var client = new Supabase.Gotrue.Client(gotrueOptions); - var url = Supabase.Gotrue.StatelessClient.SignIn(Supabase.Gotrue.Client.Provider.Spotify, gotrueOptions); + var url = client.SignIn(Provider.Spotify); Assert.IsNotNull(url); } @@ -55,7 +57,7 @@ public void CanOverrideInternalHeaders() } }; - var gotrueOptions = GetAuthOptions(supabaseUrl, "456", options); + var gotrueOptions = GetAuthOptions(supabaseUrl, "456", options); Assert.AreEqual("Bearer 123", gotrueOptions.Headers["Authorization"]); } diff --git a/SupabaseTests/Stubs/FakeAuthClient.cs b/SupabaseTests/Stubs/FakeAuthClient.cs new file mode 100644 index 00000000..d4c7800e --- /dev/null +++ b/SupabaseTests/Stubs/FakeAuthClient.cs @@ -0,0 +1,145 @@ +using Supabase.Gotrue; +using Supabase.Gotrue.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace SupabaseTests.Stubs +{ + internal class FakeAuthClient : IGotrueClient + { + public Session CurrentSession => throw new NotImplementedException(); + + public User CurrentUser => throw new NotImplementedException(); + + public Func> GetHeaders { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public event EventHandler StateChanged; + + public Task CreateUser(string jwt, AdminUserAttributes attributes) + { + throw new NotImplementedException(); + } + + public Task CreateUser(string jwt, string email, string password, AdminUserAttributes attributes = null) + { + throw new NotImplementedException(); + } + + public Task DeleteUser(string uid, string jwt) + { + throw new NotImplementedException(); + } + + public Task GetSessionFromUrl(Uri uri, bool storeSession = true) + { + throw new NotImplementedException(); + } + + public Task GetUser(string jwt) + { + throw new NotImplementedException(); + } + + public Task GetUserById(string jwt, string userId) + { + throw new NotImplementedException(); + } + + public Task InviteUserByEmail(string email, string jwt) + { + throw new NotImplementedException(); + } + + public Task> ListUsers(string jwt, string filter = null, string sortBy = null, Constants.SortOrder sortOrder = Constants.SortOrder.Descending, int? page = null, int? perPage = null) + { + throw new NotImplementedException(); + } + + public Task RefreshSession() + { + throw new NotImplementedException(); + } + + public Task ResetPasswordForEmail(string email) + { + throw new NotImplementedException(); + } + + public Task RetrieveSessionAsync() + { + throw new NotImplementedException(); + } + + public Task SendMagicLink(string email, SignInOptions options = null) + { + throw new NotImplementedException(); + } + + public Session SetAuth(string accessToken) + { + throw new NotImplementedException(); + } + + public Task SignIn(Constants.Provider provider, string scopes = null) + { + throw new NotImplementedException(); + } + + public Task SignIn(Constants.SignInType type, string identifierOrToken, string password = null, string scopes = null) + { + throw new NotImplementedException(); + } + + public Task SignIn(string email, SignInOptions options = null) + { + throw new NotImplementedException(); + } + + public Task SignIn(string email, string password) + { + throw new NotImplementedException(); + } + + public Task SignInWithPassword(string email, string password) + { + throw new NotImplementedException(); + } + + public Task SignOut() + { + throw new NotImplementedException(); + } + + public Task SignUp(Constants.SignUpType type, string identifier, string password, SignUpOptions options = null) + { + throw new NotImplementedException(); + } + + public Task SignUp(string email, string password, SignUpOptions options = null) + { + throw new NotImplementedException(); + } + + public Task Update(UserAttributes attributes) + { + throw new NotImplementedException(); + } + + public Task UpdateUserById(string jwt, string userId, AdminUserAttributes userData) + { + throw new NotImplementedException(); + } + + public Task VerifyOTP(string phone, string token, Constants.MobileOtpType type = Constants.MobileOtpType.SMS) + { + throw new NotImplementedException(); + } + + public Task VerifyOTP(string email, string token, Constants.EmailOtpType type = Constants.EmailOtpType.MagicLink) + { + throw new NotImplementedException(); + } + } +} diff --git a/SupabaseTests/Stubs/FakeFunctionsClient.cs b/SupabaseTests/Stubs/FakeFunctionsClient.cs new file mode 100644 index 00000000..e63a23f0 --- /dev/null +++ b/SupabaseTests/Stubs/FakeFunctionsClient.cs @@ -0,0 +1,29 @@ +using Supabase.Functions.Interfaces; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace SupabaseTests.Stubs +{ + internal class FakeFunctionsClient : IFunctionsClient + { + public Func> GetHeaders { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public Task Invoke(string url, string token = null, Supabase.Functions.Client.InvokeFunctionOptions options = null) + { + throw new NotImplementedException(); + } + + public Task Invoke(string url, string token = null, Supabase.Functions.Client.InvokeFunctionOptions options = null) where T : class + { + throw new NotImplementedException(); + } + + public Task RawInvoke(string url, string token = null, Supabase.Functions.Client.InvokeFunctionOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/SupabaseTests/Stubs/FakeRealtimeClient.cs b/SupabaseTests/Stubs/FakeRealtimeClient.cs new file mode 100644 index 00000000..d37052c8 --- /dev/null +++ b/SupabaseTests/Stubs/FakeRealtimeClient.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json; +using Supabase.Realtime; +using Supabase.Realtime.Interfaces; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; + +namespace SupabaseTests.Stubs +{ + internal class FakeRealtimeClient : IRealtimeClient + { + public ClientOptions Options => throw new NotImplementedException(); + + public JsonSerializerSettings SerializerSettings => throw new NotImplementedException(); + + public IRealtimeSocket Socket => throw new NotImplementedException(); + + public ReadOnlyDictionary Subscriptions => throw new NotImplementedException(); + + public event EventHandler OnClose; + public event EventHandler OnError; + public event EventHandler OnMessage; + public event EventHandler OnOpen; + + public Channel Channel(string database = "realtime", string schema = null, string table = null, string column = null, string value = null, Dictionary parameters = null) + { + throw new NotImplementedException(); + } + + public IRealtimeClient Connect(Action> callback = null) + { + throw new NotImplementedException(); + } + + public Task> ConnectAsync() + { + throw new NotImplementedException(); + } + + public IRealtimeClient Disconnect(WebSocketCloseStatus code = WebSocketCloseStatus.NormalClosure, string reason = "Programmatic Disconnect") + { + throw new NotImplementedException(); + } + + public void Remove(Channel channel) + { + throw new NotImplementedException(); + } + + void IRealtimeClient.SetAuth(string jwt) + { + throw new NotImplementedException(); + } + } +} diff --git a/SupabaseTests/Stubs/FakeRestClient.cs b/SupabaseTests/Stubs/FakeRestClient.cs new file mode 100644 index 00000000..c091d537 --- /dev/null +++ b/SupabaseTests/Stubs/FakeRestClient.cs @@ -0,0 +1,30 @@ +using Postgrest; +using Postgrest.Interfaces; +using Postgrest.Models; +using Postgrest.Responses; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace SupabaseTests.Stubs +{ + internal class FakeRestClient : IPostgrestClient + { + public string BaseUrl => throw new NotImplementedException(); + + public ClientOptions Options => throw new NotImplementedException(); + + public Func> GetHeaders { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public Task Rpc(string procedureName, Dictionary parameters) + { + throw new NotImplementedException(); + } + + public IPostgrestTable Table() where T : BaseModel, new() + { + throw new NotImplementedException(); + } + } +} diff --git a/SupabaseTests/Stubs/FakeStorageClient.cs b/SupabaseTests/Stubs/FakeStorageClient.cs new file mode 100644 index 00000000..d3be41a7 --- /dev/null +++ b/SupabaseTests/Stubs/FakeStorageClient.cs @@ -0,0 +1,50 @@ +using Storage.Interfaces; +using Supabase.Storage; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace SupabaseTests.Stubs +{ + internal class FakeStorageClient : IStorageClient + { + public Dictionary Headers { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public Func> GetHeaders { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public Task CreateBucket(string id, BucketUpsertOptions options = null) + { + throw new NotImplementedException(); + } + + public Task DeleteBucket(string id) + { + throw new NotImplementedException(); + } + + public Task EmptyBucket(string id) + { + throw new NotImplementedException(); + } + + public IStorageFileApi From(string id) + { + throw new NotImplementedException(); + } + + public Task GetBucket(string id) + { + throw new NotImplementedException(); + } + + public Task> ListBuckets() + { + throw new NotImplementedException(); + } + + public Task UpdateBucket(string id, BucketUpsertOptions options = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/SupabaseTests/SupabaseTests.csproj b/SupabaseTests/SupabaseTests.csproj index b36c33a3..f073c532 100644 --- a/SupabaseTests/SupabaseTests.csproj +++ b/SupabaseTests/SupabaseTests.csproj @@ -17,7 +17,6 @@ - diff --git a/SupabaseTests/db/01-auth-schema.sql b/SupabaseTests/db/01-auth-schema.sql index 82ab841a..0be00b58 100644 --- a/SupabaseTests/db/01-auth-schema.sql +++ b/SupabaseTests/db/01-auth-schema.sql @@ -1,80 +1,68 @@ - -CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_admin; - +CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION postgres; -- auth.users definition - CREATE TABLE auth.users ( - instance_id uuid NULL, - id uuid NOT NULL UNIQUE, - aud varchar(255) NULL, - "role" varchar(255) NULL, - email varchar(255) NULL UNIQUE, - encrypted_password varchar(255) NULL, - confirmed_at timestamptz NULL, - invited_at timestamptz NULL, - confirmation_token varchar(255) NULL, - confirmation_sent_at timestamptz NULL, - recovery_token varchar(255) NULL, - recovery_sent_at timestamptz NULL, - email_change_token varchar(255) NULL, - email_change varchar(255) NULL, - email_change_sent_at timestamptz NULL, - last_sign_in_at timestamptz NULL, - raw_app_meta_data jsonb NULL, - raw_user_meta_data jsonb NULL, - is_super_admin bool NULL, - created_at timestamptz NULL, - updated_at timestamptz NULL, - CONSTRAINT users_pkey PRIMARY KEY (id) + instance_id uuid NULL, + id uuid NOT NULL, + aud varchar(255) NULL, + "role" varchar(255) NULL, + email varchar(255) NULL, + encrypted_password varchar(255) NULL, + confirmed_at timestamptz NULL, + invited_at timestamptz NULL, + confirmation_token varchar(255) NULL, + confirmation_sent_at timestamptz NULL, + recovery_token varchar(255) NULL, + recovery_sent_at timestamptz NULL, + email_change_token varchar(255) NULL, + email_change varchar(255) NULL, + email_change_sent_at timestamptz NULL, + last_sign_in_at timestamptz NULL, + raw_app_meta_data jsonb NULL, + raw_user_meta_data jsonb NULL, + is_super_admin bool NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + CONSTRAINT users_pkey PRIMARY KEY (id) ); CREATE INDEX users_instance_id_email_idx ON auth.users USING btree (instance_id, email); CREATE INDEX users_instance_id_idx ON auth.users USING btree (instance_id); - -- auth.refresh_tokens definition - CREATE TABLE auth.refresh_tokens ( - instance_id uuid NULL, - id bigserial NOT NULL, - "token" varchar(255) NULL, - user_id varchar(255) NULL, - revoked bool NULL, - created_at timestamptz NULL, - updated_at timestamptz NULL, - CONSTRAINT refresh_tokens_pkey PRIMARY KEY (id) + instance_id uuid NULL, + id bigserial NOT NULL, + "token" varchar(255) NULL, + user_id varchar(255) NULL, + revoked bool NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + CONSTRAINT refresh_tokens_pkey PRIMARY KEY (id) ); CREATE INDEX refresh_tokens_instance_id_idx ON auth.refresh_tokens USING btree (instance_id); CREATE INDEX refresh_tokens_instance_id_user_id_idx ON auth.refresh_tokens USING btree (instance_id, user_id); CREATE INDEX refresh_tokens_token_idx ON auth.refresh_tokens USING btree (token); - -- auth.instances definition - CREATE TABLE auth.instances ( - id uuid NOT NULL, - uuid uuid NULL, - raw_base_config text NULL, - created_at timestamptz NULL, - updated_at timestamptz NULL, - CONSTRAINT instances_pkey PRIMARY KEY (id) + id uuid NOT NULL, + uuid uuid NULL, + raw_base_config text NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + CONSTRAINT instances_pkey PRIMARY KEY (id) ); - -- auth.audit_log_entries definition - CREATE TABLE auth.audit_log_entries ( - instance_id uuid NULL, - id uuid NOT NULL, - payload json NULL, - created_at timestamptz NULL, - CONSTRAINT audit_log_entries_pkey PRIMARY KEY (id) + instance_id uuid NULL, + id uuid NOT NULL, + payload json NULL, + created_at timestamptz NULL, + CONSTRAINT audit_log_entries_pkey PRIMARY KEY (id) ); CREATE INDEX audit_logs_instance_id_idx ON auth.audit_log_entries USING btree (instance_id); - -- auth.schema_migrations definition - CREATE TABLE auth.schema_migrations ( - "version" varchar(255) NOT NULL, - CONSTRAINT schema_migrations_pkey PRIMARY KEY ("version") + "version" varchar(255) NOT NULL, + CONSTRAINT schema_migrations_pkey PRIMARY KEY ("version") ); - INSERT INTO auth.schema_migrations (version) VALUES ('20171026211738'), ('20171026211808'), @@ -83,20 +71,15 @@ VALUES ('20171026211738'), ('20180108183307'), ('20180119214651'), ('20180125194653'); - -- Gets the User ID from the request cookie create or replace function auth.uid() returns uuid as $$ select nullif(current_setting('request.jwt.claim.sub', true), '')::uuid; $$ language sql stable; - -- Gets the User ID from the request cookie create or replace function auth.role() returns text as $$ select nullif(current_setting('request.jwt.claim.role', true), '')::text; $$ language sql stable; - --- Supabase super admin -CREATE USER supabase_auth_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; -GRANT ALL PRIVILEGES ON SCHEMA auth TO supabase_auth_admin; -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA auth TO supabase_auth_admin; -GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA auth TO supabase_auth_admin; -ALTER USER supabase_auth_admin SET search_path = "auth"; +GRANT ALL PRIVILEGES ON SCHEMA auth TO postgres; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA auth TO postgres; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA auth TO postgres; +ALTER USER postgres SET search_path = "auth"; \ No newline at end of file diff --git a/SupabaseTests/db/02-rest-schema.sql b/SupabaseTests/db/02-rest-schema.sql new file mode 100644 index 00000000..519b143a --- /dev/null +++ b/SupabaseTests/db/02-rest-schema.sql @@ -0,0 +1,9 @@ +-- CHANNELS +CREATE TABLE public.channels ( + id int generated by default as identity, + inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, + updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, + data jsonb DEFAULT null, + slug text +); +ALTER TABLE public.channels REPLICA IDENTITY FULL; \ No newline at end of file diff --git a/SupabaseTests/db/02-storage-schema.sql b/SupabaseTests/db/02-storage-schema.sql deleted file mode 100644 index fdbe3be4..00000000 --- a/SupabaseTests/db/02-storage-schema.sql +++ /dev/null @@ -1,116 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS storage AUTHORIZATION supabase_admin; - -grant usage on schema storage to postgres, anon, authenticated, service_role; -alter default privileges in schema storage grant all on tables to postgres, anon, authenticated, service_role; -alter default privileges in schema storage grant all on functions to postgres, anon, authenticated, service_role; -alter default privileges in schema storage grant all on sequences to postgres, anon, authenticated, service_role; - -DROP TABLE IF EXISTS "storage"."buckets"; -CREATE TABLE "storage"."buckets" ( - "id" text not NULL, - "name" text NOT NULL, - "owner" uuid, - "created_at" timestamptz DEFAULT now(), - "updated_at" timestamptz DEFAULT now(), - CONSTRAINT "buckets_owner_fkey" FOREIGN KEY ("owner") REFERENCES "auth"."users"("id"), - PRIMARY KEY ("id") -); -CREATE UNIQUE INDEX "bname" ON "storage"."buckets" USING BTREE ("name"); - -DROP TABLE IF EXISTS "storage"."objects"; -CREATE TABLE "storage"."objects" ( - "id" uuid NOT NULL DEFAULT extensions.uuid_generate_v4(), - "bucket_id" text, - "name" text, - "owner" uuid, - "created_at" timestamptz DEFAULT now(), - "updated_at" timestamptz DEFAULT now(), - "last_accessed_at" timestamptz DEFAULT now(), - "metadata" jsonb, - CONSTRAINT "objects_bucketId_fkey" FOREIGN KEY ("bucket_id") REFERENCES "storage"."buckets"("id"), - CONSTRAINT "objects_owner_fkey" FOREIGN KEY ("owner") REFERENCES "auth"."users"("id"), - PRIMARY KEY ("id") -); -CREATE UNIQUE INDEX "bucketid_objname" ON "storage"."objects" USING BTREE ("bucket_id","name"); -CREATE INDEX name_prefix_search ON storage.objects(name text_pattern_ops); - -ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY; - -CREATE OR REPLACE FUNCTION storage.foldername(name text) - RETURNS text[] - LANGUAGE plpgsql -AS $function$ -DECLARE -_parts text[]; -BEGIN - select string_to_array(name, '/') into _parts; - return _parts[1:array_length(_parts,1)-1]; -END -$function$; - -CREATE OR REPLACE FUNCTION storage.filename(name text) - RETURNS text - LANGUAGE plpgsql -AS $function$ -DECLARE -_parts text[]; -BEGIN - select string_to_array(name, '/') into _parts; - return _parts[array_length(_parts,1)]; -END -$function$; - -CREATE OR REPLACE FUNCTION storage.extension(name text) - RETURNS text - LANGUAGE plpgsql -AS $function$ -DECLARE -_parts text[]; -_filename text; -BEGIN - select string_to_array(name, '/') into _parts; - select _parts[array_length(_parts,1)] into _filename; - -- @todo return the last part instead of 2 - return split_part(_filename, '.', 2); -END -$function$; - --- @todo can this query be optimised further? -CREATE OR REPLACE FUNCTION storage.search(prefix text, bucketname text, limits int DEFAULT 100, levels int DEFAULT 1, offsets int DEFAULT 0) - RETURNS TABLE ( - name text, - id uuid, - updated_at TIMESTAMPTZ, - created_at TIMESTAMPTZ, - last_accessed_at TIMESTAMPTZ, - metadata jsonb - ) - LANGUAGE plpgsql -AS $function$ -DECLARE -_bucketId text; -BEGIN - select buckets."id" from buckets where buckets.name=bucketname limit 1 into _bucketId; - return query - with files_folders as ( - select ((string_to_array(objects.name, '/'))[levels]) as folder - from objects - where objects.name ilike prefix || '%' - and bucket_id = _bucketId - GROUP by folder - limit limits - offset offsets - ) - select files_folders.folder as name, objects.id, objects.updated_at, objects.created_at, objects.last_accessed_at, objects.metadata from files_folders - left join objects - on prefix || files_folders.folder = objects.name - where objects.id is null or objects.bucket_id=_bucketId; -END -$function$; - --- Supabase super admin -CREATE USER supabase_storage_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; -GRANT ALL PRIVILEGES ON SCHEMA storage TO supabase_storage_admin; -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA storage TO supabase_storage_admin; -GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA storage TO supabase_storage_admin; -ALTER USER supabase_storage_admin SET search_path = "storage"; diff --git a/SupabaseTests/db/03-dummy-data.sql b/SupabaseTests/db/03-dummy-data.sql new file mode 100644 index 00000000..28c70010 --- /dev/null +++ b/SupabaseTests/db/03-dummy-data.sql @@ -0,0 +1,5 @@ +-- insert users +INSERT INTO "auth"."users" ("instance_id", "id", "aud", "role", "email", "encrypted_password", "confirmed_at", "invited_at", "confirmation_token", "confirmation_sent_at", "recovery_token", "recovery_sent_at", "email_change_token", "email_change", "email_change_sent_at", "last_sign_in_at", "raw_app_meta_data", "raw_user_meta_data", "is_super_admin", "created_at", "updated_at") VALUES +('00000000-0000-0000-0000-000000000000', '317eadce-631a-4429-a0bb-f19a7a517b4a', 'authenticated', 'authenticated', 'inian+user2@supabase.io', '', NULL, '2021-02-17 04:41:13.408828+00', '541rn7rTZPGeGCYsp0a38g', '2021-02-17 04:41:13.408828+00', '', NULL, '', '', NULL, NULL, '{"provider": "email"}', 'null', 'f', '2021-02-17 04:41:13.406912+00', '2021-02-17 04:41:13.406919+00'), +('00000000-0000-0000-0000-000000000000', '4d56e902-f0a0-4662-8448-a4d9e643c142', 'authenticated', 'authenticated', 'inian+user1@supabase.io', '', NULL, '2021-02-17 04:40:58.570482+00', 'U1HvzExEO3l7JzP-4tTxJA', '2021-02-17 04:40:58.570482+00', '', NULL, '', '', NULL, NULL, '{"provider": "email"}', 'null', 'f', '2021-02-17 04:40:58.568637+00', '2021-02-17 04:40:58.568642+00'), +('00000000-0000-0000-0000-000000000000', 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2', 'authenticated', 'authenticated', 'inian+admin@supabase.io', '', NULL, '2021-02-17 04:40:42.901743+00', '3EG99GjT_e3NC4eGEBXOjw', '2021-02-17 04:40:42.901743+00', '', NULL, '', '', NULL, NULL, '{"provider": "email"}', 'null', 'f', '2021-02-17 04:40:42.890632+00', '2021-02-17 04:40:42.890637+00'); \ No newline at end of file diff --git a/SupabaseTests/db/03-realtime-schema.sql b/SupabaseTests/db/03-realtime-schema.sql deleted file mode 100644 index 67e38f57..00000000 --- a/SupabaseTests/db/03-realtime-schema.sql +++ /dev/null @@ -1,67 +0,0 @@ -ALTER SYSTEM SET wal_level='logical'; -ALTER SYSTEM SET max_wal_senders='10'; -ALTER SYSTEM SET max_replication_slots='10'; - --- Create a second schema -CREATE SCHEMA personal; - --- USERS -CREATE TYPE public.user_status AS ENUM ('ONLINE', 'OFFLINE'); -CREATE TABLE public.users ( - username text primary key, - inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - data jsonb DEFAULT null, - age_range int4range DEFAULT null, - status user_status DEFAULT 'ONLINE'::public.user_status, - catchphrase tsvector DEFAULT null -); -ALTER TABLE public.users REPLICA IDENTITY FULL; -- Send "previous data" to supabase -COMMENT ON COLUMN public.users.data IS 'For unstructured data and prototyping.'; - --- CHANNELS -CREATE TABLE public.channels ( - id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - data jsonb DEFAULT null, - slug text -); -ALTER TABLE public.users REPLICA IDENTITY FULL; -- Send "previous data" to supabase -COMMENT ON COLUMN public.channels.data IS 'For unstructured data and prototyping.'; - --- MESSAGES -CREATE TABLE public.messages ( - id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - data jsonb DEFAULT null, - message text, - username text REFERENCES users NOT NULL, - channel_id bigint REFERENCES channels NOT NULL -); -ALTER TABLE public.messages REPLICA IDENTITY FULL; -- Send "previous data" to supabase -COMMENT ON COLUMN public.messages.data IS 'For unstructured data and prototyping.'; - --- STORED FUNCTION -CREATE FUNCTION public.get_status(name_param text) -RETURNS user_status AS $$ - SELECT status from users WHERE username=name_param; -$$ LANGUAGE SQL IMMUTABLE; - --- SECOND SCHEMA USERS -CREATE TYPE personal.user_status AS ENUM ('ONLINE', 'OFFLINE'); -CREATE TABLE personal.users( - username text primary key, - inserted_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - updated_at timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL, - data jsonb DEFAULT null, - age_range int4range DEFAULT null, - status user_status DEFAULT 'ONLINE'::public.user_status -); - --- SECOND SCHEMA STORED FUNCTION -CREATE FUNCTION personal.get_status(name_param text) -RETURNS user_status AS $$ - SELECT status from users WHERE username=name_param; -$$ LANGUAGE SQL IMMUTABLE; \ No newline at end of file diff --git a/SupabaseTests/db/04-dummy-data.sql b/SupabaseTests/db/04-dummy-data.sql deleted file mode 100644 index 808ec984..00000000 --- a/SupabaseTests/db/04-dummy-data.sql +++ /dev/null @@ -1,57 +0,0 @@ --- insert users -INSERT INTO "auth"."users" ("instance_id", "id", "aud", "role", "email", "encrypted_password", "confirmed_at", "invited_at", "confirmation_token", "confirmation_sent_at", "recovery_token", "recovery_sent_at", "email_change_token", "email_change", "email_change_sent_at", "last_sign_in_at", "raw_app_meta_data", "raw_user_meta_data", "is_super_admin", "created_at", "updated_at") VALUES -('00000000-0000-0000-0000-000000000000', '317eadce-631a-4429-a0bb-f19a7a517b4a', 'authenticated', 'authenticated', 'inian+user2@supabase.io', '', NULL, '2021-02-17 04:41:13.408828+00', '541rn7rTZPGeGCYsp0a38g', '2021-02-17 04:41:13.408828+00', '', NULL, '', '', NULL, NULL, '{"provider": "email"}', 'null', 'f', '2021-02-17 04:41:13.406912+00', '2021-02-17 04:41:13.406919+00'), -('00000000-0000-0000-0000-000000000000', '4d56e902-f0a0-4662-8448-a4d9e643c142', 'authenticated', 'authenticated', 'inian+user1@supabase.io', '', NULL, '2021-02-17 04:40:58.570482+00', 'U1HvzExEO3l7JzP-4tTxJA', '2021-02-17 04:40:58.570482+00', '', NULL, '', '', NULL, NULL, '{"provider": "email"}', 'null', 'f', '2021-02-17 04:40:58.568637+00', '2021-02-17 04:40:58.568642+00'), -('00000000-0000-0000-0000-000000000000', 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2', 'authenticated', 'authenticated', 'inian+admin@supabase.io', '', NULL, '2021-02-17 04:40:42.901743+00', '3EG99GjT_e3NC4eGEBXOjw', '2021-02-17 04:40:42.901743+00', '', NULL, '', '', NULL, NULL, '{"provider": "email"}', 'null', 'f', '2021-02-17 04:40:42.890632+00', '2021-02-17 04:40:42.890637+00'); - --- insert buckets -INSERT INTO "storage"."buckets" ("id", "name", "owner", "created_at", "updated_at") VALUES -('bucket2', 'bucket2', '4d56e902-f0a0-4662-8448-a4d9e643c142', '2021-02-17 04:43:32.770206+00', '2021-02-17 04:43:32.770206+00'), -('bucket3', 'bucket3', '4d56e902-f0a0-4662-8448-a4d9e643c142', '2021-02-17 04:43:32.770206+00', '2021-02-17 04:43:32.770206+00'), -('bucket4', 'bucket4', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-25 09:23:01.58385+00', '2021-02-25 09:23:01.58385+00'), -('bucket5', 'bucket5', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-27 03:04:25.6386+00', '2021-02-27 03:04:25.6386+00'), -('public-bucket', 'public-bucket', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-27 03:04:25.6386+00', '2021-02-27 03:04:25.6386+00'); - - --- insert objects -INSERT INTO "storage"."objects" ("id", "bucket_id", "name", "owner", "created_at", "updated_at", "last_accessed_at", "metadata") VALUES -('03e458f9-892f-4db2-8cb9-d3401a689e25', 'bucket2', 'public/sadcat-upload23.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-03-04 08:26:08.553748+00', '2021-03-04 08:26:08.553748+00', '2021-03-04 08:26:08.553748+00', '{"mimetype": "image/svg+xml", "size": 1234}'), -('070825af-a11d-44fe-9f1d-abdc76f686f2', 'bucket2', 'public/sadcat-upload.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-03-02 16:31:11.115996+00', '2021-03-02 16:31:11.115996+00', '2021-03-02 16:31:11.115996+00', '{"mimetype": "image/png", "size": 1234}'), -('0cac5609-11e1-4f21-b486-d0eeb60909f6', 'bucket2', 'curlimage.jpg', 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2', '2021-02-23 11:05:16.625075+00', '2021-02-23 11:05:16.625075+00', '2021-02-23 11:05:16.625075+00', '{"size": 1234}'), -('147c6795-94d5-4008-9d81-f7ba3b4f8a9f', 'bucket2', 'folder/only_uid.jpg', 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2', '2021-02-17 10:36:01.504227+00', '2021-02-17 11:03:03.049618+00', '2021-02-17 10:36:01.504227+00', '{"size": 1234}'), -('65a3aa9c-0ff2-4adc-85d0-eab673c27443', 'bucket2', 'authenticated/casestudy.png', 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2', '2021-02-17 10:42:19.366559+00', '2021-02-17 11:03:30.025116+00', '2021-02-17 10:42:19.366559+00', '{"size": 1234}'), -('10ABE273-D77A-4BDA-B410-6FC0CA3E6ADC', 'bucket2', 'authenticated/cat.jpg', 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2', '2021-02-17 10:42:19.366559+00', '2021-02-17 11:03:30.025116+00', '2021-02-17 10:42:19.366559+00', '{"size": 1234}'), -('1edccac7-0876-4e9f-89da-a08d2a5f654b', 'bucket2', 'authenticated/delete.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-03-02 16:31:11.115996+00', '2021-03-02 16:31:11.115996+00', '2021-03-02 16:31:11.115996+00', '{"mimetype": "image/png", "size": 1234}'), -('1a911f3c-8c1d-4661-93c1-8e065e4d757e', 'bucket2', 'authenticated/delete1.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-22 22:29:15.14732+00', '2021-02-22 22:29:15.14732+00', '2021-03-02 09:32:17.116+00', '{"mimetype": "image/png", "size": 1234}'), -('372d5d74-e24d-49dc-abe8-47d7eb226a2e', 'bucket2', 'authenticated/delete-multiple1.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-22 22:29:15.14732+00', '2021-02-22 22:29:15.14732+00', '2021-03-02 09:32:17.116+00', '{"mimetype": "image/png", "size": 1234}'), -('34811c1b-85e5-4eb6-a5e3-d607b2f6986e', 'bucket2', 'authenticated/delete-multiple2.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-22 22:29:15.14732+00', '2021-02-22 22:29:15.14732+00', '2021-03-02 09:32:17.116+00', '{"mimetype": "image/png", "size": 1234}'), -('45950ff2-d3a8-4add-8e49-bafc01198340', 'bucket2', 'authenticated/delete-multiple3.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-22 22:29:15.14732+00', '2021-02-22 22:29:15.14732+00', '2021-03-02 09:32:17.116+00', '{"mimetype": "image/png", "size": 1234}'), -('469b0216-5419-41f6-9a37-2abfd7fad29c', 'bucket2', 'authenticated/delete-multiple4.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-22 22:29:15.14732+00', '2021-02-22 22:29:15.14732+00', '2021-03-02 09:32:17.116+00', '{"mimetype": "image/png", "size": 1234}'), -('55930619-a668-4dbc-aea3-b93dfe101e7f', 'bucket2', 'authenticated/delete-multiple7.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-22 22:29:15.14732+00', '2021-02-22 22:29:15.14732+00', '2021-03-02 09:32:17.116+00', '{"mimetype": "image/png", "size": 1234}'), -('D1CE4E4F-03E2-473D-858B-301D7989B581', 'bucket2', 'authenticated/move-orig.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-22 22:29:15.14732+00', '2021-02-22 22:29:15.14732+00', '2021-03-02 09:32:17.116+00', '{"mimetype": "image/png", "size": 1234}'), -('222b3d1e-bc17-414c-b336-47894aa4d697', 'bucket2', 'authenticated/move-orig-2.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-22 22:29:15.14732+00', '2021-02-22 22:29:15.14732+00', '2021-03-02 09:32:17.116+00', '{"mimetype": "image/png", "size": 1234}'), -('8f7d643d-1e82-4d39-ae39-d9bd6b0cfe9c', 'bucket2', 'authenticated/move-orig-3.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-02-22 22:29:15.14732+00', '2021-02-22 22:29:15.14732+00', '2021-03-02 09:32:17.116+00', '{"mimetype": "image/png", "size": 1234}'), -('8377527d-3518-4dc8-8290-c6926470e795', 'bucket2', 'folder/subfolder/public-all-permissions.png', 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2', '2021-02-17 10:26:42.791214+00', '2021-02-17 11:03:30.025116+00', '2021-02-17 10:26:42.791214+00', '{"size": 1234}'), -('b39ae4ab-802b-4c42-9271-3f908c34363c', 'bucket2', 'private/sadcat-upload3.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-03-01 08:53:29.567975+00', '2021-03-01 08:53:29.567975+00', '2021-03-01 08:53:29.567975+00', '{"mimetype": "image/svg+xml", "size": 1234}'), -('8098E1AC-C744-4368-86DF-71B60CCDE221', 'bucket3', 'sadcat-upload3.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-03-01 08:53:29.567975+00', '2021-03-01 08:53:29.567975+00', '2021-03-01 08:53:29.567975+00', '{"mimetype": "image/svg+xml", "size": 1234}'), -('D3EB488E-94F4-46CD-86D3-242C13B95BAC', 'bucket3', 'sadcat-upload2.png', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-03-01 08:53:29.567975+00', '2021-03-01 08:53:29.567975+00', '2021-03-01 08:53:29.567975+00', '{"mimetype": "image/svg+xml", "size": 1234}'), -('746180e8-8029-4134-8a21-48ab35485d81', 'public-bucket', 'favicon.ico', '317eadce-631a-4429-a0bb-f19a7a517b4a', '2021-03-01 08:53:29.567975+00', '2021-03-01 08:53:29.567975+00', '2021-03-01 08:53:29.567975+00', '{"mimetype": "image/svg+xml", "size": 1234}'); -; - --- add policies --- allows user to CRUD all buckets -CREATE POLICY crud_buckets ON storage.buckets for all USING (auth.uid() = '317eadce-631a-4429-a0bb-f19a7a517b4a'); --- allow public CRUD acccess to the public folder in bucket2 -CREATE POLICY crud_public_folder ON storage.objects for all USING (bucket_id='bucket2' and (storage.foldername(name))[1] = 'public'); --- allow public CRUD acccess to a particular file in bucket2 -CREATE POLICY crud_public_file ON storage.objects for all USING (bucket_id='bucket2' and name = 'folder/subfolder/public-all-permissions.png'); --- allow public CRUD acccess to a folder in bucket2 to a user with a given id -CREATE POLICY crud_uid_folder ON storage.objects for all USING (bucket_id='bucket2' and (storage.foldername(name))[1] = 'only_uid' and auth.uid() = 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2'); --- allow public CRUD acccess to a file in bucket2 to a user with a given id -CREATE POLICY crud_uid_file ON storage.objects for all USING (bucket_id='bucket2' and name = 'folder/only_uid.jpg' and auth.uid() = 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2'); --- allow CRUD acccess to a folder in bucket2 to all authenticated users -CREATE POLICY authenticated_folder ON storage.objects for all USING (bucket_id='bucket2' and (storage.foldername(name))[1] = 'authenticated' and auth.role() = 'authenticated'); --- allow CRUD access to a folder in bucket2 to its owners -CREATE POLICY crud_owner_only ON storage.objects for all USING (bucket_id='bucket2' and (storage.foldername(name))[1] = 'only_owner' and owner = auth.uid()); --- allow CRUD access to bucket4 -CREATE POLICY open_all_update ON storage.objects for all WITH CHECK (bucket_id='bucket4'); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 21c6340e..25d5dc10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "2.3" services: db: - image: postgres:13 + image: postgres:14 restart: unless-stopped ports: - "5432:5432" @@ -19,7 +19,7 @@ services: POSTGRES_PORT: 5432 rest: - image: postgrest/postgrest:v7.0.1 + image: postgrest/postgrest:latest restart: unless-stopped ports: - "3000:3000" @@ -57,7 +57,7 @@ services: - db realtime: - image: supabase/realtime:latest + image: supabase/realtime:v0.25.1 restart: unless-stopped ports: - "4000:4000" @@ -72,31 +72,4 @@ services: JWT_SECRET: 'f023d3db-39dc-4ac9-87b2-b2be72e9162b' SECURE_CHANNELS: "false" depends_on: - - db - - storage: - image: supabase/storage-api - restart: unless-stopped - depends_on: - - db - ports: - - "5000:5000" - environment: - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxMzUzMTk4NSwiZXhwIjoxOTI5MTA3OTg1fQ.mqfi__KnQB4v6PkIjkhzfwWrYyF94MEbSC6LnuvVniE - SERVICE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjEzNTMxOTg1LCJleHAiOjE5MjkxMDc5ODV9.th84OKK0Iz8QchDyXZRrojmKSEZ-OuitQm_5DvLiSIc - PROJECT_REF: bjhaohmqunupljrqypxz - REGION: ${AWS_REGION} - GLOBAL_S3_BUCKET: ${AWS_BUCKET} - POSTGREST_URL: http://rest:3000 - PGRST_JWT_SECRET: f023d3db-39dc-4ac9-87b2-b2be72e9162b - DATABASE_URL: postgresql://postgres:postgres@db/postgres - PGOPTIONS: "-c search_path=storage,public" - FILE_SIZE_LIMIT: 52428800 - STORAGE_BACKEND: s3 - FILE_STORAGE_BACKEND_PATH: ./data - X_FORWARDED_HOST_REGEXP: - POSTGREST_URL_SUFFIX: /rest/v1 - ADMIN_API_KEYS: apikey - ENCRYPTION_KEY: encryptionkey + - db \ No newline at end of file diff --git a/modules/core-csharp b/modules/core-csharp new file mode 160000 index 00000000..e6eede93 --- /dev/null +++ b/modules/core-csharp @@ -0,0 +1 @@ +Subproject commit e6eede932871a8082abd82efe598a91bc4421082 diff --git a/modules/functions-csharp b/modules/functions-csharp index 503049fc..8d06da54 160000 --- a/modules/functions-csharp +++ b/modules/functions-csharp @@ -1 +1 @@ -Subproject commit 503049fca9a48ea94b01c0b7a497c0bbe311c036 +Subproject commit 8d06da54b89677d9f587a1f0998ac708feb5c418 diff --git a/modules/gotrue-csharp b/modules/gotrue-csharp index 088d6062..7ad15ba5 160000 --- a/modules/gotrue-csharp +++ b/modules/gotrue-csharp @@ -1 +1 @@ -Subproject commit 088d606231d19ac98a0ee145c173f988c29df19a +Subproject commit 7ad15ba5491a286df674ab755b40a253100ef698 diff --git a/modules/postgrest-csharp b/modules/postgrest-csharp index 52d9e93e..5845d4da 160000 --- a/modules/postgrest-csharp +++ b/modules/postgrest-csharp @@ -1 +1 @@ -Subproject commit 52d9e93e79808ff3fc4eba13da8bfbfa875c6bbe +Subproject commit 5845d4da3a47bd8d4e2b07d0fd693996c307e4cd diff --git a/modules/realtime-csharp b/modules/realtime-csharp index 07334bf2..ffb1b55b 160000 --- a/modules/realtime-csharp +++ b/modules/realtime-csharp @@ -1 +1 @@ -Subproject commit 07334bf2c08ba9ba5a6d9284188bd2c98fd76420 +Subproject commit ffb1b55b7f13de69c70cf080916978fc25c9823a diff --git a/modules/storage-csharp b/modules/storage-csharp index c87d0084..b3fc157d 160000 --- a/modules/storage-csharp +++ b/modules/storage-csharp @@ -1 +1 @@ -Subproject commit c87d0084c2130e5def08f5fc46a525c132b9ce5f +Subproject commit b3fc157d841df5f52694e758eb40972938006874 diff --git a/supabase-csharp.sln b/supabase-csharp.sln index dfc0bd83..e7b62812 100644 --- a/supabase-csharp.sln +++ b/supabase-csharp.sln @@ -1,31 +1,34 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32519.379 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F94175FC-DE2B-4DBC-9C79-2A97B8489412}" ProjectSection(SolutionItems) = preProject - docker-compose.yml = docker-compose.yml - README.md = README.md .env = .env .env.sample = .env.sample .gitignore = .gitignore CHANGELOG.md = CHANGELOG.md + docker-compose.yml = docker-compose.yml + README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Supabase", "Supabase\Supabase.csproj", "{FAE80407-C121-47A3-9304-D39FA828E9F1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Supabase", "Supabase\Supabase.csproj", "{FAE80407-C121-47A3-9304-D39FA828E9F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SupabaseTests", "SupabaseTests\SupabaseTests.csproj", "{28EE4F80-74AA-46F6-B15E-27C30310401A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SupabaseTests", "SupabaseTests\SupabaseTests.csproj", "{28EE4F80-74AA-46F6-B15E-27C30310401A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SupabaseExample", "SupabaseExample\SupabaseExample.csproj", "{DA3EF17E-F901-428D-B9BD-94A078E389E9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SupabaseExample", "SupabaseExample\SupabaseExample.csproj", "{DA3EF17E-F901-428D-B9BD-94A078E389E9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{43FFFE0C-91D2-43AB-913F-B3824702AE49}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{5B805377-7615-441C-86B1-4BE203B0289B}" ProjectSection(SolutionItems) = preProject .github\workflows\build-documentation.yaml = .github\workflows\build-documentation.yaml - .github\workflows\release.yml = .github\workflows\release.yml .github\workflows\dotnet-core.yml = .github\workflows\dotnet-core.yml + .github\workflows\release.yml = .github\workflows\release.yml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{E407C761-AA9C-423C-AD1C-7EE687D3CAB9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,11 +48,17 @@ Global {DA3EF17E-F901-428D-B9BD-94A078E389E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA3EF17E-F901-428D-B9BD-94A078E389E9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection - GlobalSection(MonoDevelopProperties) = preSolution - version = 0.3.4 + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {43FFFE0C-91D2-43AB-913F-B3824702AE49} = {F94175FC-DE2B-4DBC-9C79-2A97B8489412} {5B805377-7615-441C-86B1-4BE203B0289B} = {43FFFE0C-91D2-43AB-913F-B3824702AE49} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {832DE89D-7252-4B03-9301-BB8D36B40992} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + version = 0.3.4 + EndGlobalSection EndGlobal