From add52292fc67c9c23c205e2e3fd5f8c28537d483 Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Thu, 10 Nov 2022 13:30:08 -0600 Subject: [PATCH 1/9] Update child modules and update codebase to implement them --- README.md | 27 ++- Supabase/Client.cs | 290 +++++++++++++++--------------- Supabase/StatelessClient.cs | 43 +++-- Supabase/Supabase.csproj | 15 +- Supabase/SupabaseFunctions.cs | 33 ++-- Supabase/SupabaseModel.cs | 9 - Supabase/SupabaseOptions.cs | 59 ++++++ Supabase/SupabaseTable.cs | 27 ++- SupabaseExample/Models/Channel.cs | 3 +- SupabaseExample/Models/Movie.cs | 6 +- SupabaseExample/Program.cs | 20 +-- SupabaseTests/Client.cs | 12 +- SupabaseTests/Models/Channel.cs | 3 +- SupabaseTests/Models/Stub.cs | 2 +- SupabaseTests/Models/User.cs | 2 +- SupabaseTests/StatelessClient.cs | 9 +- modules/functions-csharp | 2 +- modules/gotrue-csharp | 2 +- modules/postgrest-csharp | 2 +- modules/realtime-csharp | 2 +- modules/storage-csharp | 2 +- supabase-csharp.sln | 64 +++++-- 22 files changed, 367 insertions(+), 267 deletions(-) create mode 100644 Supabase/SupabaseOptions.cs 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..68de6707 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -4,10 +4,18 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Postgrest; +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 static Supabase.Functions.Client; +using static Supabase.Gotrue.Constants; namespace Supabase { @@ -24,36 +32,85 @@ public enum ChannelEventType All } + public IGotrueClient Auth { get => AuthClient; } + public SupabaseFunctions Functions { get => new SupabaseFunctions(FunctionsClient, FunctionsUrl, GetAuthHeaders()); } + + public IPostgrestClient Postgrest { get => PostgrestClient; } + + public IRealtimeClient Realtime { get => RealtimeClient; } + + public IStorageClient Storage { get => StorageClient; } + /// /// 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 AuthClient + { + get + { + return _authClient; + } + set + { + // Remove existing internal state listener (if applicable) + if (_authClient != null) + _authClient.StateChanged -= Auth_StateChanged; + + _authClient = value; + _authClient.StateChanged += Auth_StateChanged; + } + } + private IGotrueClient _authClient; + + /// + /// Supabase Realtime allows for realtime feedback on database changes. + /// + public IRealtimeClient RealtimeClient + { + get + { + return _realtimeClient; + } + set + { + // Disconnect from previous socket (if applicable) + if (_realtimeClient != null) + _realtimeClient.Disconnect(); + + _realtimeClient = value; + } + } + private IRealtimeClient _realtimeClient; /// /// Supabase Edge functions allow you to deploy and invoke edge functions. /// - public SupabaseFunctions Functions => new SupabaseFunctions(instance.FunctionsUrl, instance.GetAuthHeaders()); + public IFunctionsClient FunctionsClient + { + get => _functionsClient; + set => _functionsClient = value; + } + private IFunctionsClient _functionsClient; - private Postgrest.Client Postgrest() => global::Postgrest.Client.Initialize(instance.RestUrl, new Postgrest.ClientOptions + /// + /// Supabase Postgrest allows for strongly typed REST interactions with the your database. + /// + public IPostgrestClient PostgrestClient { - Headers = instance.GetAuthHeaders(), - Schema = Schema - }); + get => _postgrestClient; + set => _postgrestClient = value; + } + private IPostgrestClient _postgrestClient; - private static Client instance; - public static Client Instance + /// + /// Supabase Storage allows you to manage user-generated content, such as photos or videos. + /// + public IStorageClient StorageClient { - get - { - if (instance == null) - { - Debug.WriteLine("Supabase must be initialized before it is called."); - return null; - } - return instance; - } + get => _storageClient; + set => _storageClient = value; } + private IStorageClient _storageClient; public string SupabaseKey { get; private set; } public string SupabaseUrl { get; private set; } @@ -66,48 +123,20 @@ public static Client Instance private SupabaseOptions options; - private Client() { } - - - /// - /// Initializes a Supabase Client. - /// - /// - /// - /// - /// - public static void Initialize(string supabaseUrl, string supabaseKey, SupabaseOptions options = null, Action callback = null) - { - Task.Run(async () => - { - var result = await InitializeAsync(supabaseUrl, supabaseKey, options); - callback?.Invoke(result); - }); - } - - /// - /// Initializes a Supabase Client Asynchronously. - /// - /// - /// - /// - /// - 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; + SupabaseUrl = supabaseUrl; + 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; + AuthUrl = string.Format(options.AuthUrlFormat, supabaseUrl); + RestUrl = string.Format(options.RestUrlFormat, supabaseUrl); + RealtimeUrl = string.Format(options.RealtimeUrlFormat, supabaseUrl).Replace("http", "ws"); + StorageUrl = string.Format(options.StorageUrlFormat, supabaseUrl); + 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); @@ -115,81 +144,98 @@ public static async Task InitializeAsync(string supabaseUrl, string supa 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, + Headers = GetAuthHeaders(), AutoRefreshToken = options.AutoRefreshToken, PersistSession = options.PersistSession, SessionDestroyer = options.SessionDestroyer, SessionPersistor = options.SessionPersistor, SessionRetriever = options.SessionRetriever - }); - instance.Auth.StateChanged += Auth_StateChanged; + }; + + _authClient = new Gotrue.Client(gotrueOptions); + _authClient.StateChanged += Auth_StateChanged; // 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 = SupabaseKey } + }; + + _realtimeClient = new Realtime.Client(RealtimeUrl, realtimeOptions); + + _postgrestClient = new Postgrest.Client(RestUrl, new Postgrest.ClientOptions + { + Headers = GetAuthHeaders(), + Schema = Schema + }); + + _functionsClient = new Functions.Client(); + + _storageClient = new Storage.Client(StorageUrl, GetAuthHeaders()); + } + + + /// + /// Initializes a Supabase Client. + /// + /// + /// + /// + /// + public async Task InitializeAsync() + { + await AuthClient.RetrieveSessionAsync(); - return instance; + if (options.AutoConnectRealtime) + { + await RealtimeClient.ConnectAsync(); + } + return this; } - private static void Auth_StateChanged(object sender, ClientStateChanged e) + 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) + case AuthState.SignedIn: + case AuthState.TokenRefreshed: + if (AuthClient.CurrentSession != null && AuthClient.CurrentSession.AccessToken != null) { - Instance.Realtime.SetAuth(Instance.Auth.CurrentSession.AccessToken); + RealtimeClient.SetAuth(AuthClient.CurrentSession.AccessToken); } + _postgrestClient.Options.Headers = GetAuthHeaders(); + _storageClient.Headers = GetAuthHeaders(); 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 RealtimeClient.Subscriptions.Values) + subscription.Unsubscribe(); + RealtimeClient.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 SupabaseTable From() where T : BaseModel, new() => new SupabaseTable(Postgrest, Realtime); /// /// Runs a remote procedure. @@ -197,7 +243,7 @@ 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() @@ -213,63 +259,11 @@ internal Dictionary GetAuthHeaders() } else { - var bearer = Auth?.CurrentSession?.AccessToken != null ? Auth.CurrentSession.AccessToken : SupabaseKey; + var bearer = AuthClient?.CurrentSession?.AccessToken != null ? AuthClient.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/StatelessClient.cs b/Supabase/StatelessClient.cs index 8130928f..d4e7188f 100644 --- a/Supabase/StatelessClient.cs +++ b/Supabase/StatelessClient.cs @@ -16,28 +16,28 @@ 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) { if (options == null) options = new SupabaseOptions(); - var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); + 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); + 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,12 +51,12 @@ public static Postgrest.StatelessClientOptions GetRestOptions(string supabaseUrl /// /// /// - public static Storage.Client Storage(string supabaseUrl, string supabaseKey = null, SupabaseOptions options = null) + public static Storage.Client Storage(string supabaseUrl, string? supabaseKey = null, SupabaseOptions? options = null) { if (options == null) options = new SupabaseOptions(); - var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); + var headers = GetAuthHeaders(supabaseKey ?? "", options).MergeLeft(options.Headers); return new Storage.Client(string.Format(options.StorageUrlFormat, supabaseUrl), headers); } @@ -68,7 +68,7 @@ public static Storage.Client Storage(string supabaseUrl, string supabaseKey = nu /// /// /// - public static SupabaseFunctions Functions(string supabaseUrl, string supabaseKey, SupabaseOptions options = null) + public static SupabaseFunctions Functions(string supabaseUrl, string supabaseKey, SupabaseOptions? options = null) { if (options == null) options = new SupabaseOptions(); @@ -88,8 +88,9 @@ public static SupabaseFunctions Functions(string supabaseUrl, string supabaseKey } var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); + var client = new Functions.Client(); - return new SupabaseFunctions(functionsUrl, headers); + return new SupabaseFunctions(client, functionsUrl, headers); } /// @@ -97,19 +98,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,12 +123,12 @@ 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); } diff --git a/Supabase/Supabase.csproj b/Supabase/Supabase.csproj index 9229a430..bd81efda 100644 --- a/Supabase/Supabase.csproj +++ b/Supabase/Supabase.csproj @@ -19,18 +19,23 @@ 0.5.3 + + enable + 8.0 + CS8600;CS8602;CS8603 + 0.5.3 $(VersionPrefix) - + - - - + + + - + diff --git a/Supabase/SupabaseFunctions.cs b/Supabase/SupabaseFunctions.cs index 375319cf..45810e7a 100644 --- a/Supabase/SupabaseFunctions.cs +++ b/Supabase/SupabaseFunctions.cs @@ -1,4 +1,5 @@ -using System; +using Supabase.Functions.Interfaces; +using System; using System.Collections.Generic; using System.Net.Http; using System.Text; @@ -9,31 +10,33 @@ namespace Supabase { public class SupabaseFunctions { - private string _functionsUrl; - private Dictionary _headers = new Dictionary(); + private string functionsUrl; + private Dictionary headers = new Dictionary(); + private IFunctionsClient client; - public SupabaseFunctions(string functionsUrl, Dictionary headers) + public SupabaseFunctions(IFunctionsClient client, string functionsUrl, Dictionary headers) { - _functionsUrl = functionsUrl.TrimEnd('/'); - _headers = headers; + this.client = client; + this.functionsUrl = functionsUrl.TrimEnd('/'); + this.headers = headers; } - public Task Invoke(string functionName, Dictionary body = null) => Functions.Client.Invoke($"{_functionsUrl}/{functionName}", options: new InvokeFunctionOptions + public Task Invoke(string functionName, Dictionary? body = null) => client.Invoke($"{functionsUrl}/{functionName}", options: new InvokeFunctionOptions { - Headers = _headers, - Body = body + Headers = headers, + Body = body ?? new Dictionary() }); - public Task Invoke(string functionName, Dictionary body = null) => Functions.Client.Invoke($"{_functionsUrl}/{functionName}", options: new InvokeFunctionOptions + public Task Invoke(string functionName, Dictionary? body = null) where T : class => client.Invoke($"{functionsUrl}/{functionName}", options: new InvokeFunctionOptions { - Headers = _headers, - Body = body + Headers = headers, + Body = body ?? new Dictionary() }); - public Task RawInvoke(string functionName, Dictionary body = null) => Functions.Client.RawInvoke($"{_functionsUrl}/{functionName}", options: new InvokeFunctionOptions + public Task RawInvoke(string functionName, Dictionary? body = null) => client.RawInvoke($"{functionsUrl}/{functionName}", options: new InvokeFunctionOptions { - Headers = _headers, - Body = body + Headers = headers, + Body = body ?? new Dictionary() }); } } diff --git a/Supabase/SupabaseModel.cs b/Supabase/SupabaseModel.cs index 79e49880..a258beb5 100644 --- a/Supabase/SupabaseModel.cs +++ b/Supabase/SupabaseModel.cs @@ -8,14 +8,5 @@ namespace Supabase { 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..7b6a39a9 --- /dev/null +++ b/Supabase/SupabaseOptions.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Supabase.Gotrue; + +namespace Supabase +{ + /// + /// 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/SupabaseTable.cs b/Supabase/SupabaseTable.cs index a3e28ab4..4a737c6f 100644 --- a/Supabase/SupabaseTable.cs +++ b/Supabase/SupabaseTable.cs @@ -2,21 +2,30 @@ using System.Collections.Generic; using System.Threading.Tasks; using Postgrest; +using Postgrest.Interfaces; using Postgrest.Models; using Supabase.Realtime; +using Supabase.Realtime.Interfaces; using static Supabase.Client; namespace Supabase { public class SupabaseTable : Table where T : 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, null, postgrestClient.Options) + { + this.postgrestClient = postgrestClient; + this.realtimeClient = realtimeClient; + this.schema = schema; + } public async Task On(ChannelEventType e, Action action) { @@ -25,17 +34,17 @@ public async Task On(ChannelEventType e, Action(); // In regard to: https://github.com/supabase/supabase-js/pull/270 - var headers = Instance.GetAuthHeaders(); + var headers = postgrestClient.Options.Headers; if (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", this.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) { 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..07e794ec 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, ShouldInitializeRealtime = 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..c22a9a61 100644 --- a/SupabaseTests/Client.cs +++ b/SupabaseTests/Client.cs @@ -13,10 +13,10 @@ 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,7 +27,8 @@ 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", @@ -35,13 +36,14 @@ public async Task InitializeTest() ShouldInitializeRealtime = true, AutoConnectRealtime = true }); + await Instance.InitializeAsync(); } [TestMethod("Client: Initializes.")] public void ClientInitializes() { - Assert.IsNotNull(Instance.Realtime); - Assert.IsNotNull(Instance.Auth); + Assert.IsNotNull(Instance.RealtimeClient); + Assert.IsNotNull(Instance.AuthClient); } //[TestMethod("Client: Connects to Realtime")] 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..74bfc30a 100644 --- a/SupabaseTests/StatelessClient.cs +++ b/SupabaseTests/StatelessClient.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using SupabaseTests.Models; +using static Supabase.Gotrue.Constants; using static Supabase.StatelessClient; namespace SupabaseTests @@ -22,8 +23,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(supabaseUrl, restOptions).Table().Get(); var result2 = await From(supabaseUrl, null, options).Get(); @@ -35,9 +36,9 @@ public void CanAccessStatelessGotrue() { 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); } diff --git a/modules/functions-csharp b/modules/functions-csharp index 503049fc..563f37b2 160000 --- a/modules/functions-csharp +++ b/modules/functions-csharp @@ -1 +1 @@ -Subproject commit 503049fca9a48ea94b01c0b7a497c0bbe311c036 +Subproject commit 563f37b2921880d63ffa8a9a2f7c769b2bffc41e diff --git a/modules/gotrue-csharp b/modules/gotrue-csharp index 088d6062..7a5ad805 160000 --- a/modules/gotrue-csharp +++ b/modules/gotrue-csharp @@ -1 +1 @@ -Subproject commit 088d606231d19ac98a0ee145c173f988c29df19a +Subproject commit 7a5ad805e8d7b8f9b9e2709918289e2084fecf8c diff --git a/modules/postgrest-csharp b/modules/postgrest-csharp index 52d9e93e..8c16b71e 160000 --- a/modules/postgrest-csharp +++ b/modules/postgrest-csharp @@ -1 +1 @@ -Subproject commit 52d9e93e79808ff3fc4eba13da8bfbfa875c6bbe +Subproject commit 8c16b71edd9ea30bf70c513a39828cd6eff18d07 diff --git a/modules/realtime-csharp b/modules/realtime-csharp index 07334bf2..3d35c4e2 160000 --- a/modules/realtime-csharp +++ b/modules/realtime-csharp @@ -1 +1 @@ -Subproject commit 07334bf2c08ba9ba5a6d9284188bd2c98fd76420 +Subproject commit 3d35c4e2f0c7145c5c50fda76a1cf41d18e8dbda diff --git a/modules/storage-csharp b/modules/storage-csharp index c87d0084..16346d05 160000 --- a/modules/storage-csharp +++ b/modules/storage-csharp @@ -1 +1 @@ -Subproject commit c87d0084c2130e5def08f5fc46a525c132b9ce5f +Subproject commit 16346d056611dc8fe3674a3bc7ea794fd3ecbcd6 diff --git a/supabase-csharp.sln b/supabase-csharp.sln index dfc0bd83..17a564dc 100644 --- a/supabase-csharp.sln +++ b/supabase-csharp.sln @@ -1,31 +1,44 @@ - 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 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Storage", "modules\storage-csharp\Storage\Storage.csproj", "{A4D9D516-0269-4EB7-8D4F-53C6F2334438}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions", "modules\functions-csharp\Functions\Functions.csproj", "{4B1AC83B-4249-4F86-874C-AA0C771CC5B5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Postgrest", "modules\postgrest-csharp\Postgrest\Postgrest.csproj", "{41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gotrue", "modules\gotrue-csharp\Gotrue\Gotrue.csproj", "{AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Realtime", "modules\realtime-csharp\Realtime\Realtime.csproj", "{4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,12 +57,43 @@ Global {DA3EF17E-F901-428D-B9BD-94A078E389E9}.Debug|Any CPU.Build.0 = Debug|Any CPU {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 + {A4D9D516-0269-4EB7-8D4F-53C6F2334438}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4D9D516-0269-4EB7-8D4F-53C6F2334438}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4D9D516-0269-4EB7-8D4F-53C6F2334438}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4D9D516-0269-4EB7-8D4F-53C6F2334438}.Release|Any CPU.Build.0 = Release|Any CPU + {4B1AC83B-4249-4F86-874C-AA0C771CC5B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B1AC83B-4249-4F86-874C-AA0C771CC5B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B1AC83B-4249-4F86-874C-AA0C771CC5B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B1AC83B-4249-4F86-874C-AA0C771CC5B5}.Release|Any CPU.Build.0 = Release|Any CPU + {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}.Release|Any CPU.Build.0 = Release|Any CPU + {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}.Release|Any CPU.Build.0 = Release|Any CPU + {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}.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} + {A4D9D516-0269-4EB7-8D4F-53C6F2334438} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} + {4B1AC83B-4249-4F86-874C-AA0C771CC5B5} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} + {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} + {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} + {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {832DE89D-7252-4B03-9301-BB8D36B40992} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + version = 0.3.4 EndGlobalSection EndGlobal From a9f8a599e97a6bc799e4307d931e65d4cc9953b4 Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Thu, 10 Nov 2022 13:55:01 -0600 Subject: [PATCH 2/9] Create interfaces for publicly available classes. --- Supabase/Client.cs | 15 +++----- Supabase/Interfaces/ISupabaseClient.cs | 47 +++++++++++++++++++++++ Supabase/Interfaces/ISupabaseFunctions.cs | 13 +++++++ Supabase/Interfaces/ISupabaseTable.cs | 16 ++++++++ Supabase/SupabaseFunctions.cs | 3 +- Supabase/SupabaseModel.cs | 9 ++--- Supabase/SupabaseTable.cs | 4 +- supabase-csharp.sln | 35 ----------------- 8 files changed, 91 insertions(+), 51 deletions(-) create mode 100644 Supabase/Interfaces/ISupabaseClient.cs create mode 100644 Supabase/Interfaces/ISupabaseFunctions.cs create mode 100644 Supabase/Interfaces/ISupabaseTable.cs diff --git a/Supabase/Client.cs b/Supabase/Client.cs index 68de6707..a35714b4 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -1,9 +1,6 @@ -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; @@ -11,10 +8,10 @@ using Supabase.Functions.Interfaces; using Supabase.Gotrue; using Supabase.Gotrue.Interfaces; +using Supabase.Interfaces; using Supabase.Realtime; using Supabase.Realtime.Interfaces; using Supabase.Storage; -using static Supabase.Functions.Client; using static Supabase.Gotrue.Constants; namespace Supabase @@ -22,7 +19,7 @@ namespace Supabase /// /// A singleton class representing a Supabase Client. /// - public class Client + public class Client : ISupabaseClient { public enum ChannelEventType { @@ -194,7 +191,7 @@ public Client(string supabaseUrl, string supabaseKey, SupabaseOptions? options = /// /// /// - public async Task InitializeAsync() + public async Task> InitializeAsync() { await AuthClient.RetrieveSessionAsync(); @@ -233,9 +230,9 @@ private void Auth_StateChanged(object sender, ClientStateChanged e) /// /// Gets the Postgrest client to prepare for a query. /// - /// + /// /// - public SupabaseTable From() where T : BaseModel, new() => new SupabaseTable(Postgrest, Realtime); + public ISupabaseTable From() where TModel : BaseModel, new() => new SupabaseTable(Postgrest, Realtime); /// /// Runs a remote procedure. diff --git a/Supabase/Interfaces/ISupabaseClient.cs b/Supabase/Interfaces/ISupabaseClient.cs new file mode 100644 index 00000000..3599d7aa --- /dev/null +++ b/Supabase/Interfaces/ISupabaseClient.cs @@ -0,0 +1,47 @@ +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; } + IGotrueClient AuthClient { get; set; } + string AuthUrl { get; } + SupabaseFunctions Functions { get; } + IFunctionsClient FunctionsClient { get; set; } + string FunctionsUrl { get; } + IPostgrestClient Postgrest { get; } + IPostgrestClient PostgrestClient { get; set; } + IRealtimeClient Realtime { get; } + IRealtimeClient RealtimeClient { get; set; } + string RealtimeUrl { get; } + string RestUrl { get; } + string Schema { get; } + IStorageClient Storage { get; } + IStorageClient StorageClient { get; set; } + string StorageUrl { get; } + string SupabaseKey { get; } + string SupabaseUrl { get; } + + 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/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/SupabaseFunctions.cs b/Supabase/SupabaseFunctions.cs index 45810e7a..5ed70559 100644 --- a/Supabase/SupabaseFunctions.cs +++ b/Supabase/SupabaseFunctions.cs @@ -1,4 +1,5 @@ using Supabase.Functions.Interfaces; +using Supabase.Interfaces; using System; using System.Collections.Generic; using System.Net.Http; @@ -8,7 +9,7 @@ namespace Supabase { - public class SupabaseFunctions + public class SupabaseFunctions : ISupabaseFunctions { private string functionsUrl; private Dictionary headers = new Dictionary(); diff --git a/Supabase/SupabaseModel.cs b/Supabase/SupabaseModel.cs index a258beb5..42e42da7 100644 --- a/Supabase/SupabaseModel.cs +++ b/Supabase/SupabaseModel.cs @@ -1,8 +1,7 @@ -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 { diff --git a/Supabase/SupabaseTable.cs b/Supabase/SupabaseTable.cs index 4a737c6f..eaf5657d 100644 --- a/Supabase/SupabaseTable.cs +++ b/Supabase/SupabaseTable.cs @@ -4,13 +4,15 @@ 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; diff --git a/supabase-csharp.sln b/supabase-csharp.sln index 17a564dc..e7b62812 100644 --- a/supabase-csharp.sln +++ b/supabase-csharp.sln @@ -29,16 +29,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{E407C761-AA9C-423C-AD1C-7EE687D3CAB9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Storage", "modules\storage-csharp\Storage\Storage.csproj", "{A4D9D516-0269-4EB7-8D4F-53C6F2334438}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions", "modules\functions-csharp\Functions\Functions.csproj", "{4B1AC83B-4249-4F86-874C-AA0C771CC5B5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Postgrest", "modules\postgrest-csharp\Postgrest\Postgrest.csproj", "{41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gotrue", "modules\gotrue-csharp\Gotrue\Gotrue.csproj", "{AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Realtime", "modules\realtime-csharp\Realtime\Realtime.csproj", "{4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,26 +47,6 @@ Global {DA3EF17E-F901-428D-B9BD-94A078E389E9}.Debug|Any CPU.Build.0 = Debug|Any CPU {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 - {A4D9D516-0269-4EB7-8D4F-53C6F2334438}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4D9D516-0269-4EB7-8D4F-53C6F2334438}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A4D9D516-0269-4EB7-8D4F-53C6F2334438}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4D9D516-0269-4EB7-8D4F-53C6F2334438}.Release|Any CPU.Build.0 = Release|Any CPU - {4B1AC83B-4249-4F86-874C-AA0C771CC5B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4B1AC83B-4249-4F86-874C-AA0C771CC5B5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4B1AC83B-4249-4F86-874C-AA0C771CC5B5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4B1AC83B-4249-4F86-874C-AA0C771CC5B5}.Release|Any CPU.Build.0 = Release|Any CPU - {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3}.Release|Any CPU.Build.0 = Release|Any CPU - {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF}.Release|Any CPU.Build.0 = Release|Any CPU - {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -84,11 +54,6 @@ Global GlobalSection(NestedProjects) = preSolution {43FFFE0C-91D2-43AB-913F-B3824702AE49} = {F94175FC-DE2B-4DBC-9C79-2A97B8489412} {5B805377-7615-441C-86B1-4BE203B0289B} = {43FFFE0C-91D2-43AB-913F-B3824702AE49} - {A4D9D516-0269-4EB7-8D4F-53C6F2334438} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} - {4B1AC83B-4249-4F86-874C-AA0C771CC5B5} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} - {41E2DDC6-A0EF-4330-A6DD-82B426CAF9D3} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} - {AB06B258-3DE5-4C58-BAEA-566EEF2ED7DF} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} - {4924CE6F-0142-4B91-A0A4-E5D3E8DAFEED} = {E407C761-AA9C-423C-AD1C-7EE687D3CAB9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {832DE89D-7252-4B03-9301-BB8D36B40992} From 8309cc396c080eac5aff78bbd97fa6b88c496552 Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Thu, 10 Nov 2022 21:07:21 -0600 Subject: [PATCH 3/9] Update tests and stub fake DI clients. --- Supabase/Client.cs | 1 - Supabase/Supabase.csproj | 2 +- Supabase/SupabaseTable.cs | 2 +- SupabaseTests/Client.cs | 57 ++++---- SupabaseTests/StatelessClient.cs | 2 +- SupabaseTests/Stubs/FakeAuthClient.cs | 143 +++++++++++++++++++++ SupabaseTests/Stubs/FakeFunctionsClient.cs | 27 ++++ SupabaseTests/Stubs/FakeRealtimeClient.cs | 58 +++++++++ SupabaseTests/Stubs/FakeRestClient.cs | 28 ++++ SupabaseTests/Stubs/FakeStorageClient.cs | 49 +++++++ SupabaseTests/SupabaseTests.csproj | 1 - SupabaseTests/db/01-auth-schema.sql | 113 +++++++--------- SupabaseTests/db/02-rest-schema.sql | 9 ++ SupabaseTests/db/02-storage-schema.sql | 116 ----------------- SupabaseTests/db/03-dummy-data.sql | 5 + SupabaseTests/db/03-realtime-schema.sql | 67 ---------- SupabaseTests/db/04-dummy-data.sql | 57 -------- docker-compose.yml | 35 +---- modules/postgrest-csharp | 2 +- 19 files changed, 409 insertions(+), 365 deletions(-) create mode 100644 SupabaseTests/Stubs/FakeAuthClient.cs create mode 100644 SupabaseTests/Stubs/FakeFunctionsClient.cs create mode 100644 SupabaseTests/Stubs/FakeRealtimeClient.cs create mode 100644 SupabaseTests/Stubs/FakeRestClient.cs create mode 100644 SupabaseTests/Stubs/FakeStorageClient.cs create mode 100644 SupabaseTests/db/02-rest-schema.sql delete mode 100644 SupabaseTests/db/02-storage-schema.sql create mode 100644 SupabaseTests/db/03-dummy-data.sql delete mode 100644 SupabaseTests/db/03-realtime-schema.sql delete mode 100644 SupabaseTests/db/04-dummy-data.sql diff --git a/Supabase/Client.cs b/Supabase/Client.cs index a35714b4..8f49ff2d 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -242,7 +242,6 @@ private void Auth_StateChanged(object sender, ClientStateChanged e) /// public Task Rpc(string procedureName, Dictionary parameters) => Postgrest.Rpc(procedureName, parameters); - internal Dictionary GetAuthHeaders() { var headers = new Dictionary(); diff --git a/Supabase/Supabase.csproj b/Supabase/Supabase.csproj index bd81efda..a3b5f349 100644 --- a/Supabase/Supabase.csproj +++ b/Supabase/Supabase.csproj @@ -31,7 +31,7 @@ - + diff --git a/Supabase/SupabaseTable.cs b/Supabase/SupabaseTable.cs index eaf5657d..66b81343 100644 --- a/Supabase/SupabaseTable.cs +++ b/Supabase/SupabaseTable.cs @@ -22,7 +22,7 @@ public class SupabaseTable : Table, ISupabaseTable realtimeClient, string schema = "public") : base(postgrestClient.BaseUrl, null, postgrestClient.Options) + 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; diff --git a/SupabaseTests/Client.cs b/SupabaseTests/Client.cs index c22a9a61..4be8ebe3 100644 --- a/SupabaseTests/Client.cs +++ b/SupabaseTests/Client.cs @@ -1,12 +1,9 @@ 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 { @@ -34,7 +31,7 @@ public async Task InitializeTest() RealtimeUrlFormat = "{0}:4000/socket", RestUrlFormat = "{0}:3000", ShouldInitializeRealtime = true, - AutoConnectRealtime = true + AutoConnectRealtime = true, }); await Instance.InitializeAsync(); } @@ -46,29 +43,27 @@ public void ClientInitializes() Assert.IsNotNull(Instance.AuthClient); } - //[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() @@ -100,5 +95,21 @@ public async Task SupabaseModelDeletes() Assert.AreEqual(0, result.Models.Count); } + + [TestMethod("Supports Dependency Injection for clients via property")] + public void SupportsDIForClientsViaProperty() + { + Instance.AuthClient = new FakeAuthClient(); + Instance.FunctionsClient = new FakeFunctionsClient(); + Instance.RealtimeClient = new FakeRealtimeClient(); + Instance.PostgrestClient = new FakeRestClient(); + Instance.StorageClient = 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/StatelessClient.cs b/SupabaseTests/StatelessClient.cs index 74bfc30a..65e43e41 100644 --- a/SupabaseTests/StatelessClient.cs +++ b/SupabaseTests/StatelessClient.cs @@ -24,7 +24,7 @@ public class StatelessClient public async Task CanAccessStatelessRest() { var restOptions = GetRestOptions(null, options); - var result1 = await new Postgrest.Client(supabaseUrl, restOptions).Table().Get(); + var result1 = await new Postgrest.Client(String.Format(options.RestUrlFormat, supabaseUrl), restOptions).Table().Get(); var result2 = await From(supabaseUrl, null, options).Get(); diff --git a/SupabaseTests/Stubs/FakeAuthClient.cs b/SupabaseTests/Stubs/FakeAuthClient.cs new file mode 100644 index 00000000..32386e42 --- /dev/null +++ b/SupabaseTests/Stubs/FakeAuthClient.cs @@ -0,0 +1,143 @@ +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 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..c057ec97 --- /dev/null +++ b/SupabaseTests/Stubs/FakeFunctionsClient.cs @@ -0,0 +1,27 @@ +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 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..8e872cb7 --- /dev/null +++ b/SupabaseTests/Stubs/FakeRestClient.cs @@ -0,0 +1,28 @@ +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 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..0f04cc0c --- /dev/null +++ b/SupabaseTests/Stubs/FakeStorageClient.cs @@ -0,0 +1,49 @@ +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 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/postgrest-csharp b/modules/postgrest-csharp index 8c16b71e..f9df7ab5 160000 --- a/modules/postgrest-csharp +++ b/modules/postgrest-csharp @@ -1 +1 @@ -Subproject commit 8c16b71edd9ea30bf70c513a39828cd6eff18d07 +Subproject commit f9df7ab55370b7aa08cbf9884d62eac58dc9cd25 From c16e6598cf9cba7970918a04c5953cc260289578 Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Thu, 10 Nov 2022 23:30:18 -0600 Subject: [PATCH 4/9] Implemented changes suggested by @veleek --- Supabase/Client.cs | 65 ++++++++++--------- Supabase/DefaultSupabaseSessionHandler.cs | 17 +++++ Supabase/Interfaces/ISupabaseClient.cs | 8 --- .../Interfaces/ISupabaseSessionHandler.cs | 26 ++++++++ Supabase/StatelessClient.cs | 18 +++-- Supabase/SupabaseModel.cs | 1 + Supabase/SupabaseOptions.cs | 26 ++------ Supabase/SupabaseTable.cs | 8 +-- SupabaseExample/Program.cs | 2 +- SupabaseTests/Client.cs | 1 - 10 files changed, 100 insertions(+), 72 deletions(-) create mode 100644 Supabase/DefaultSupabaseSessionHandler.cs create mode 100644 Supabase/Interfaces/ISupabaseSessionHandler.cs diff --git a/Supabase/Client.cs b/Supabase/Client.cs index 8f49ff2d..0ed09b38 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -30,7 +30,7 @@ public enum ChannelEventType } public IGotrueClient Auth { get => AuthClient; } - public SupabaseFunctions Functions { get => new SupabaseFunctions(FunctionsClient, FunctionsUrl, GetAuthHeaders()); } + public SupabaseFunctions Functions { get => new SupabaseFunctions(FunctionsClient, functionsUrl, GetAuthHeaders()); } public IPostgrestClient Postgrest { get => PostgrestClient; } @@ -109,31 +109,29 @@ public IStorageClient StorageClient } private IStorageClient _storageClient; - 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; } + private string? supabaseKey; + private string authUrl; + private string restUrl; + private string realtimeUrl; + private string storageUrl; + private string functionsUrl; + private string schema; private SupabaseOptions options; - public Client(string supabaseUrl, string supabaseKey, SupabaseOptions? options = null) + public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options = null) { - SupabaseUrl = supabaseUrl; - SupabaseKey = supabaseKey; + this.supabaseKey = supabaseKey; options ??= new SupabaseOptions(); this.options = options; - AuthUrl = string.Format(options.AuthUrlFormat, supabaseUrl); - RestUrl = string.Format(options.RestUrlFormat, supabaseUrl); - RealtimeUrl = string.Format(options.RealtimeUrlFormat, supabaseUrl).Replace("http", "ws"); - StorageUrl = string.Format(options.StorageUrlFormat, supabaseUrl); - Schema = options.Schema; + authUrl = string.Format(options.AuthUrlFormat, supabaseUrl); + restUrl = string.Format(options.RestUrlFormat, supabaseUrl); + realtimeUrl = string.Format(options.RealtimeUrlFormat, supabaseUrl).Replace("http", "ws"); + storageUrl = string.Format(options.StorageUrlFormat, supabaseUrl); + 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); @@ -141,23 +139,23 @@ public Client(string supabaseUrl, string supabaseKey, SupabaseOptions? options = if (isPlatform.Success) { var parts = supabaseUrl.Split('.'); - FunctionsUrl = $"{parts[0]}.functions.{parts[1]}.{parts[2]}"; + functionsUrl = $"{parts[0]}.functions.{parts[1]}.{parts[2]}"; } else { - FunctionsUrl = string.Format(options.FunctionsUrlFormat, supabaseUrl); + functionsUrl = string.Format(options.FunctionsUrlFormat, supabaseUrl); } // Init Auth var gotrueOptions = new Gotrue.ClientOptions { - Url = AuthUrl, + Url = authUrl, Headers = GetAuthHeaders(), AutoRefreshToken = options.AutoRefreshToken, PersistSession = options.PersistSession, - SessionDestroyer = options.SessionDestroyer, - SessionPersistor = options.SessionPersistor, - SessionRetriever = options.SessionRetriever + SessionDestroyer = options.SessionHandler.SessionDestroyer, + SessionPersistor = options.SessionHandler.SessionPersistor, + SessionRetriever = options.SessionHandler.SessionRetriever }; _authClient = new Gotrue.Client(gotrueOptions); @@ -167,20 +165,20 @@ public Client(string supabaseUrl, string supabaseKey, SupabaseOptions? options = var realtimeOptions = new Realtime.ClientOptions { - Parameters = { ApiKey = SupabaseKey } + Parameters = { ApiKey = this.supabaseKey } }; - _realtimeClient = new Realtime.Client(RealtimeUrl, realtimeOptions); + _realtimeClient = new Realtime.Client(realtimeUrl, realtimeOptions); - _postgrestClient = new Postgrest.Client(RestUrl, new Postgrest.ClientOptions + _postgrestClient = new Postgrest.Client(restUrl, new Postgrest.ClientOptions { Headers = GetAuthHeaders(), - Schema = Schema + Schema = schema }); _functionsClient = new Functions.Client(); - _storageClient = new Storage.Client(StorageUrl, GetAuthHeaders()); + _storageClient = new Storage.Client(storageUrl, GetAuthHeaders()); } @@ -210,7 +208,7 @@ private void Auth_StateChanged(object sender, ClientStateChanged e) // Ref: https://github.com/supabase-community/supabase-csharp/issues/12 case AuthState.SignedIn: case AuthState.TokenRefreshed: - if (AuthClient.CurrentSession != null && AuthClient.CurrentSession.AccessToken != null) + if (AuthClient.CurrentSession?.AccessToken != null) { RealtimeClient.SetAuth(AuthClient.CurrentSession.AccessToken); } @@ -245,9 +243,14 @@ private void Auth_StateChanged(object sender, ClientStateChanged e) internal Dictionary GetAuthHeaders() { var headers = new Dictionary(); - headers["apiKey"] = SupabaseKey; + headers["X-Client-Info"] = Util.GetAssemblyVersion(); + if (supabaseKey != null) + { + headers["apiKey"] = supabaseKey; + } + // In Regard To: https://github.com/supabase/supabase-csharp/issues/5 if (options.Headers.ContainsKey("Authorization")) { @@ -255,7 +258,7 @@ internal Dictionary GetAuthHeaders() } else { - var bearer = AuthClient?.CurrentSession?.AccessToken != null ? AuthClient.CurrentSession.AccessToken : SupabaseKey; + var bearer = AuthClient.CurrentSession?.AccessToken ?? supabaseKey; headers["Authorization"] = $"Bearer {bearer}"; } diff --git a/Supabase/DefaultSupabaseSessionHandler.cs b/Supabase/DefaultSupabaseSessionHandler.cs new file mode 100644 index 00000000..391568f2 --- /dev/null +++ b/Supabase/DefaultSupabaseSessionHandler.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using Supabase.Gotrue; +using Supabase.Interfaces; + +namespace Supabase +{ + public class DefaultSupabaseSessionHandler : ISupabaseSessionHandler + { + public Task SessionPersistor(Session session) => Task.FromResult(true); + + + public Task SessionRetriever() => Task.FromResult(null); + + + public Task SessionDestroyer() => Task.FromResult(true); + } +} diff --git a/Supabase/Interfaces/ISupabaseClient.cs b/Supabase/Interfaces/ISupabaseClient.cs index 3599d7aa..4c649591 100644 --- a/Supabase/Interfaces/ISupabaseClient.cs +++ b/Supabase/Interfaces/ISupabaseClient.cs @@ -23,22 +23,14 @@ public interface ISupabaseClient Auth { get; } IGotrueClient AuthClient { get; set; } - string AuthUrl { get; } SupabaseFunctions Functions { get; } IFunctionsClient FunctionsClient { get; set; } - string FunctionsUrl { get; } IPostgrestClient Postgrest { get; } IPostgrestClient PostgrestClient { get; set; } IRealtimeClient Realtime { get; } IRealtimeClient RealtimeClient { get; set; } - string RealtimeUrl { get; } - string RestUrl { get; } - string Schema { get; } IStorageClient Storage { get; } IStorageClient StorageClient { get; set; } - string StorageUrl { get; } - string SupabaseKey { get; } - string SupabaseUrl { get; } ISupabaseTable From() where TModel : BaseModel, new(); Task> InitializeAsync(); diff --git a/Supabase/Interfaces/ISupabaseSessionHandler.cs b/Supabase/Interfaces/ISupabaseSessionHandler.cs new file mode 100644 index 00000000..8e2aac11 --- /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(Session session); + + /// + /// Function to retrieve a session (probably from the filesystem or cookie) + /// + Task SessionRetriever(); + + /// + /// Function to destroy a session. + /// + Task SessionDestroyer(); + } +} diff --git a/Supabase/StatelessClient.cs b/Supabase/StatelessClient.cs index d4e7188f..a61b201b 100644 --- a/Supabase/StatelessClient.cs +++ b/Supabase/StatelessClient.cs @@ -21,7 +21,7 @@ public static Gotrue.ClientOptions GetAuthOptions(string supabaseUrl, string? su if (options == null) options = new SupabaseOptions(); - var headers = GetAuthHeaders(supabaseKey ?? "", options).MergeLeft(options.Headers); + var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); return new Gotrue.ClientOptions { @@ -35,7 +35,7 @@ public static Postgrest.ClientOptions GetRestOptions(string? supabaseKey = null, if (options == null) options = new SupabaseOptions(); - var headers = GetAuthHeaders(supabaseKey ?? "", options).MergeLeft(options.Headers); + var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); return new Postgrest.ClientOptions { @@ -56,7 +56,7 @@ public static Storage.Client Storage(string supabaseUrl, string? supabaseKey = n if (options == null) options = new SupabaseOptions(); - var headers = GetAuthHeaders(supabaseKey ?? "", options).MergeLeft(options.Headers); + var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); return new Storage.Client(string.Format(options.StorageUrlFormat, supabaseUrl), headers); } @@ -132,12 +132,17 @@ public static Task Rpc(string supabaseUrl, string supabaseKey, str } - 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(); + if (supabaseKey != null) + { + headers["apiKey"] = supabaseKey; + } + // In Regard To: https://github.com/supabase/supabase-csharp/issues/5 if (options.Headers.ContainsKey("Authorization")) { @@ -145,8 +150,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/SupabaseModel.cs b/Supabase/SupabaseModel.cs index 42e42da7..318fec5e 100644 --- a/Supabase/SupabaseModel.cs +++ b/Supabase/SupabaseModel.cs @@ -5,6 +5,7 @@ namespace Supabase { + [Obsolete] public abstract class SupabaseModel : BaseModel { } diff --git a/Supabase/SupabaseOptions.cs b/Supabase/SupabaseOptions.cs index 7b6a39a9..e84b2aea 100644 --- a/Supabase/SupabaseOptions.cs +++ b/Supabase/SupabaseOptions.cs @@ -1,7 +1,6 @@ -using System; +using Supabase.Interfaces; +using System; using System.Collections.Generic; -using System.Threading.Tasks; -using Supabase.Gotrue; namespace Supabase { @@ -17,11 +16,6 @@ public class SupabaseOptions /// public bool AutoRefreshToken { get; set; } = true; - /// - /// Should the Client Initialize Realtime? - /// - public bool ShouldInitializeRealtime { get; set; } = false; - /// /// Should the Client automatically connect to Realtime? /// @@ -33,19 +27,11 @@ public class SupabaseOptions 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. + /// Functions passed to Gotrue that handle sessions. + /// + /// **By default these do nothing for persistence.** /// - public Func> SessionDestroyer = () => Task.FromResult(true); + public ISupabaseSessionHandler SessionHandler { get; set; } = new DefaultSupabaseSessionHandler(); public Dictionary Headers = new Dictionary(); diff --git a/Supabase/SupabaseTable.cs b/Supabase/SupabaseTable.cs index 66b81343..1b64b8a7 100644 --- a/Supabase/SupabaseTable.cs +++ b/Supabase/SupabaseTable.cs @@ -51,16 +51,16 @@ public async Task On(ChannelEventType e, Action 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/SupabaseExample/Program.cs b/SupabaseExample/Program.cs index 07e794ec..2f0c8ac0 100644 --- a/SupabaseExample/Program.cs +++ b/SupabaseExample/Program.cs @@ -14,7 +14,7 @@ static async Task Main(string[] args) var url = Environment.GetEnvironmentVariable("SUPABASE_URL"); var key = Environment.GetEnvironmentVariable("SUPABASE_KEY"); - var supabase = new Supabase.Client(url, key, new Supabase.SupabaseOptions { AutoConnectRealtime = true, ShouldInitializeRealtime = true }); + var supabase = new Supabase.Client(url, key, new Supabase.SupabaseOptions { AutoConnectRealtime = true }); await supabase.InitializeAsync(); var reference = supabase.From(); diff --git a/SupabaseTests/Client.cs b/SupabaseTests/Client.cs index 4be8ebe3..71911dc4 100644 --- a/SupabaseTests/Client.cs +++ b/SupabaseTests/Client.cs @@ -30,7 +30,6 @@ public async Task InitializeTest() AuthUrlFormat = "{0}:9999", RealtimeUrlFormat = "{0}:4000/socket", RestUrlFormat = "{0}:3000", - ShouldInitializeRealtime = true, AutoConnectRealtime = true, }); await Instance.InitializeAsync(); From 4ac9a8c10d3c3ef28f2e479d3547dae16ccf8c9d Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Thu, 10 Nov 2022 23:34:17 -0600 Subject: [PATCH 5/9] Fix regression on `GetAuthHeaders` --- Supabase/Client.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Supabase/Client.cs b/Supabase/Client.cs index 0ed09b38..e482c05d 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -258,7 +258,7 @@ internal Dictionary GetAuthHeaders() } else { - var bearer = AuthClient.CurrentSession?.AccessToken ?? supabaseKey; + var bearer = AuthClient.CurrentSession?.AccessToken != null ? AuthClient.CurrentSession.AccessToken : supabaseKey; headers["Authorization"] = $"Bearer {bearer}"; } From 6645e38fececdb4d5980d50062159d608c60a127 Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Sat, 12 Nov 2022 18:28:31 -0600 Subject: [PATCH 6/9] Implemented changes suggested by @veleek --- .gitmodules | 3 + Supabase/Client.cs | 105 ++++++++---------- Supabase/DefaultSupabaseSessionHandler.cs | 6 +- Supabase/Interfaces/ISupabaseClient.cs | 27 ++--- .../Interfaces/ISupabaseSessionHandler.cs | 4 +- Supabase/StatelessClient.cs | 17 ++- Supabase/Supabase.csproj | 9 +- Supabase/SupabaseFunctions.cs | 43 ------- Supabase/SupabaseOptions.cs | 3 +- SupabaseTests/Client.cs | 14 +-- SupabaseTests/StatelessClient.cs | 5 +- SupabaseTests/Stubs/FakeAuthClient.cs | 2 + SupabaseTests/Stubs/FakeFunctionsClient.cs | 2 + SupabaseTests/Stubs/FakeRestClient.cs | 2 + SupabaseTests/Stubs/FakeStorageClient.cs | 1 + modules/core-csharp | 1 + modules/functions-csharp | 2 +- modules/gotrue-csharp | 2 +- modules/postgrest-csharp | 2 +- modules/realtime-csharp | 2 +- modules/storage-csharp | 2 +- 21 files changed, 108 insertions(+), 146 deletions(-) delete mode 100644 Supabase/SupabaseFunctions.cs create mode 160000 modules/core-csharp 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/Supabase/Client.cs b/Supabase/Client.cs index e482c05d..2874fa10 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -29,85 +29,76 @@ public enum ChannelEventType All } - public IGotrueClient Auth { get => AuthClient; } - public SupabaseFunctions Functions { get => new SupabaseFunctions(FunctionsClient, functionsUrl, GetAuthHeaders()); } - - public IPostgrestClient Postgrest { get => PostgrestClient; } - - public IRealtimeClient Realtime { get => RealtimeClient; } - - public IStorageClient Storage { get => StorageClient; } - /// /// Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies. /// - public IGotrueClient AuthClient + public IGotrueClient Auth { get { - return _authClient; + return _auth; } set { // Remove existing internal state listener (if applicable) - if (_authClient != null) - _authClient.StateChanged -= Auth_StateChanged; + if (_auth != null) + _auth.StateChanged -= Auth_StateChanged; - _authClient = value; - _authClient.StateChanged += Auth_StateChanged; + _auth = value; + _auth.StateChanged += Auth_StateChanged; } } - private IGotrueClient _authClient; + private IGotrueClient _auth; /// /// Supabase Realtime allows for realtime feedback on database changes. /// - public IRealtimeClient RealtimeClient + public IRealtimeClient Realtime { get { - return _realtimeClient; + return _realtime; } set { // Disconnect from previous socket (if applicable) - if (_realtimeClient != null) - _realtimeClient.Disconnect(); + if (_realtime != null) + _realtime.Disconnect(); - _realtimeClient = value; + _realtime = value; } } - private IRealtimeClient _realtimeClient; + private IRealtimeClient _realtime; /// /// Supabase Edge functions allow you to deploy and invoke edge functions. /// - public IFunctionsClient FunctionsClient + public IFunctionsClient Functions { - get => _functionsClient; - set => _functionsClient = value; + get => _functions; + set => _functions = value; } - private IFunctionsClient _functionsClient; + private IFunctionsClient _functions; /// /// Supabase Postgrest allows for strongly typed REST interactions with the your database. /// - public IPostgrestClient PostgrestClient + public IPostgrestClient Postgrest { - get => _postgrestClient; - set => _postgrestClient = value; + get => _postgrest; + set => _postgrest = value; } - private IPostgrestClient _postgrestClient; + private IPostgrestClient _postgrest; /// /// Supabase Storage allows you to manage user-generated content, such as photos or videos. /// - public IStorageClient StorageClient + public IStorageClient Storage { - get => _storageClient; - set => _storageClient = value; + get => _storage; + set => _storage = value; } - private IStorageClient _storageClient; + private IStorageClient _storage; private string? supabaseKey; private string authUrl; @@ -147,19 +138,20 @@ public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options } // Init Auth - var gotrueOptions = new Gotrue.ClientOptions + var gotrueOptions = new Gotrue.ClientOptions { Url = authUrl, - Headers = GetAuthHeaders(), AutoRefreshToken = options.AutoRefreshToken, PersistSession = options.PersistSession, SessionDestroyer = options.SessionHandler.SessionDestroyer, SessionPersistor = options.SessionHandler.SessionPersistor, - SessionRetriever = options.SessionHandler.SessionRetriever + SessionRetriever = options.SessionHandler.SessionRetriever }; - _authClient = new Gotrue.Client(gotrueOptions); - _authClient.StateChanged += Auth_StateChanged; + _auth = new Gotrue.Client(gotrueOptions); + _auth.StateChanged += Auth_StateChanged; + _auth.GetHeaders = () => GetAuthHeaders(); + // Init Realtime @@ -168,17 +160,16 @@ public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options Parameters = { ApiKey = this.supabaseKey } }; - _realtimeClient = new Realtime.Client(realtimeUrl, realtimeOptions); + _realtime = new Realtime.Client(realtimeUrl, realtimeOptions); - _postgrestClient = new Postgrest.Client(restUrl, new Postgrest.ClientOptions - { - Headers = GetAuthHeaders(), - Schema = schema - }); + _postgrest = new Postgrest.Client(restUrl, new Postgrest.ClientOptions { Schema = schema }); + _postgrest.GetHeaders = () => GetAuthHeaders(); - _functionsClient = new Functions.Client(); + _functions = new Functions.Client(functionsUrl); + _functions.GetHeaders = () => GetAuthHeaders(); - _storageClient = new Storage.Client(storageUrl, GetAuthHeaders()); + _storage = new Storage.Client(storageUrl, GetAuthHeaders()); + _storage.GetHeaders = () => GetAuthHeaders(); } @@ -191,11 +182,11 @@ public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options /// public async Task> InitializeAsync() { - await AuthClient.RetrieveSessionAsync(); + await Auth.RetrieveSessionAsync(); if (options.AutoConnectRealtime) { - await RealtimeClient.ConnectAsync(); + await Realtime.ConnectAsync(); } return this; } @@ -208,19 +199,19 @@ private void Auth_StateChanged(object sender, ClientStateChanged e) // Ref: https://github.com/supabase-community/supabase-csharp/issues/12 case AuthState.SignedIn: case AuthState.TokenRefreshed: - if (AuthClient.CurrentSession?.AccessToken != null) + if (Auth.CurrentSession?.AccessToken != null) { - RealtimeClient.SetAuth(AuthClient.CurrentSession.AccessToken); + Realtime.SetAuth(Auth.CurrentSession.AccessToken); } - _postgrestClient.Options.Headers = GetAuthHeaders(); - _storageClient.Headers = GetAuthHeaders(); + _postgrest.Options.Headers = GetAuthHeaders(); + _storage.Headers = GetAuthHeaders(); break; // Remove Realtime Subscriptions on Auth Signout. case AuthState.SignedOut: - foreach (var subscription in RealtimeClient.Subscriptions.Values) + foreach (var subscription in Realtime.Subscriptions.Values) subscription.Unsubscribe(); - RealtimeClient.Disconnect(); + Realtime.Disconnect(); break; } } @@ -238,7 +229,7 @@ private 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() { @@ -258,7 +249,7 @@ internal Dictionary GetAuthHeaders() } else { - var bearer = AuthClient.CurrentSession?.AccessToken != null ? AuthClient.CurrentSession.AccessToken : supabaseKey; + var bearer = Auth.CurrentSession?.AccessToken != null ? Auth.CurrentSession.AccessToken : supabaseKey; headers["Authorization"] = $"Bearer {bearer}"; } diff --git a/Supabase/DefaultSupabaseSessionHandler.cs b/Supabase/DefaultSupabaseSessionHandler.cs index 391568f2..fcce965c 100644 --- a/Supabase/DefaultSupabaseSessionHandler.cs +++ b/Supabase/DefaultSupabaseSessionHandler.cs @@ -6,12 +6,12 @@ namespace Supabase { public class DefaultSupabaseSessionHandler : ISupabaseSessionHandler { - public Task SessionPersistor(Session session) => Task.FromResult(true); + public Task SessionPersistor(TSession session) where TSession : Session => Task.FromResult(true); - public Task SessionRetriever() => Task.FromResult(null); + public Task SessionRetriever() where TSession : Session => Task.FromResult(null); - public Task SessionDestroyer() => Task.FromResult(true); + public Task SessionDestroyer() => Task.FromResult(true); } } diff --git a/Supabase/Interfaces/ISupabaseClient.cs b/Supabase/Interfaces/ISupabaseClient.cs index 4c649591..2f345153 100644 --- a/Supabase/Interfaces/ISupabaseClient.cs +++ b/Supabase/Interfaces/ISupabaseClient.cs @@ -14,23 +14,18 @@ namespace Supabase.Interfaces { public interface ISupabaseClient - where TUser: User - where TSession: Session - where TSocket: IRealtimeSocket - where TChannel: IRealtimeChannel - where TBucket: Bucket - where TFileObject: FileObject + where TUser : User + where TSession : Session + where TSocket : IRealtimeSocket + where TChannel : IRealtimeChannel + where TBucket : Bucket + where TFileObject : FileObject { - IGotrueClient Auth { get; } - IGotrueClient AuthClient { get; set; } - SupabaseFunctions Functions { get; } - IFunctionsClient FunctionsClient { get; set; } - IPostgrestClient Postgrest { get; } - IPostgrestClient PostgrestClient { get; set; } - IRealtimeClient Realtime { get; } - IRealtimeClient RealtimeClient { get; set; } - IStorageClient Storage { get; } - IStorageClient StorageClient { get; set; } + 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(); diff --git a/Supabase/Interfaces/ISupabaseSessionHandler.cs b/Supabase/Interfaces/ISupabaseSessionHandler.cs index 8e2aac11..3761ccf7 100644 --- a/Supabase/Interfaces/ISupabaseSessionHandler.cs +++ b/Supabase/Interfaces/ISupabaseSessionHandler.cs @@ -11,12 +11,12 @@ public interface ISupabaseSessionHandler /// /// Function called to persist the session (probably on a filesystem or cookie) /// - Task SessionPersistor(Session session); + Task SessionPersistor(TSession session) where TSession : Session; /// /// Function to retrieve a session (probably from the filesystem or cookie) /// - Task SessionRetriever(); + Task SessionRetriever() where TSession : Session; /// /// Function to destroy a session. diff --git a/Supabase/StatelessClient.cs b/Supabase/StatelessClient.cs index a61b201b..5b6d7392 100644 --- a/Supabase/StatelessClient.cs +++ b/Supabase/StatelessClient.cs @@ -6,8 +6,11 @@ using Postgrest; using Postgrest.Models; using Postgrest.Responses; +using Storage.Interfaces; using Supabase.Extensions; +using Supabase.Functions.Interfaces; using Supabase.Gotrue; +using Supabase.Storage; namespace Supabase { @@ -16,14 +19,15 @@ namespace Supabase /// public static class StatelessClient { - public static Gotrue.ClientOptions 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.ClientOptions + return new Gotrue.ClientOptions { Url = string.Format(options.AuthUrlFormat, supabaseUrl), Headers = headers @@ -51,7 +55,7 @@ public static Postgrest.ClientOptions GetRestOptions(string? supabaseKey = null, /// /// /// - 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 +72,7 @@ public static Storage.Client Storage(string supabaseUrl, string? supabaseKey = n /// /// /// - 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,9 +92,10 @@ public static SupabaseFunctions Functions(string supabaseUrl, string supabaseKey } var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers); - var client = new Functions.Client(); + var client = new Functions.Client(functionsUrl); + client.GetHeaders = () => headers; - return new SupabaseFunctions(client, functionsUrl, headers); + return client; } /// diff --git a/Supabase/Supabase.csproj b/Supabase/Supabase.csproj index a3b5f349..a7996194 100644 --- a/Supabase/Supabase.csproj +++ b/Supabase/Supabase.csproj @@ -29,13 +29,14 @@ $(VersionPrefix) - + - + - + - + + diff --git a/Supabase/SupabaseFunctions.cs b/Supabase/SupabaseFunctions.cs deleted file mode 100644 index 5ed70559..00000000 --- a/Supabase/SupabaseFunctions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Supabase.Functions.Interfaces; -using Supabase.Interfaces; -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 : ISupabaseFunctions - { - private string functionsUrl; - private Dictionary headers = new Dictionary(); - private IFunctionsClient client; - - public SupabaseFunctions(IFunctionsClient client, string functionsUrl, Dictionary headers) - { - this.client = client; - this.functionsUrl = functionsUrl.TrimEnd('/'); - this.headers = headers; - } - - public Task Invoke(string functionName, Dictionary? body = null) => client.Invoke($"{functionsUrl}/{functionName}", options: new InvokeFunctionOptions - { - Headers = headers, - Body = body ?? new Dictionary() - }); - - public Task Invoke(string functionName, Dictionary? body = null) where T : class => client.Invoke($"{functionsUrl}/{functionName}", options: new InvokeFunctionOptions - { - Headers = headers, - Body = body ?? new Dictionary() - }); - - public Task RawInvoke(string functionName, Dictionary? body = null) => client.RawInvoke($"{functionsUrl}/{functionName}", options: new InvokeFunctionOptions - { - Headers = headers, - Body = body ?? new Dictionary() - }); - } -} diff --git a/Supabase/SupabaseOptions.cs b/Supabase/SupabaseOptions.cs index e84b2aea..4829fd1d 100644 --- a/Supabase/SupabaseOptions.cs +++ b/Supabase/SupabaseOptions.cs @@ -1,4 +1,5 @@ -using Supabase.Interfaces; +using Supabase.Gotrue; +using Supabase.Interfaces; using System; using System.Collections.Generic; diff --git a/SupabaseTests/Client.cs b/SupabaseTests/Client.cs index 71911dc4..773ceee8 100644 --- a/SupabaseTests/Client.cs +++ b/SupabaseTests/Client.cs @@ -38,8 +38,8 @@ public async Task InitializeTest() [TestMethod("Client: Initializes.")] public void ClientInitializes() { - Assert.IsNotNull(Instance.RealtimeClient); - Assert.IsNotNull(Instance.AuthClient); + Assert.IsNotNull(Instance.Realtime); + Assert.IsNotNull(Instance.Auth); } [TestMethod("Client: Connects to Realtime")] @@ -98,11 +98,11 @@ public async Task SupabaseModelDeletes() [TestMethod("Supports Dependency Injection for clients via property")] public void SupportsDIForClientsViaProperty() { - Instance.AuthClient = new FakeAuthClient(); - Instance.FunctionsClient = new FakeFunctionsClient(); - Instance.RealtimeClient = new FakeRealtimeClient(); - Instance.PostgrestClient = new FakeRestClient(); - Instance.StorageClient = new FakeStorageClient(); + 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("")); diff --git a/SupabaseTests/StatelessClient.cs b/SupabaseTests/StatelessClient.cs index 65e43e41..f2cde036 100644 --- a/SupabaseTests/StatelessClient.cs +++ b/SupabaseTests/StatelessClient.cs @@ -2,6 +2,7 @@ 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; @@ -34,7 +35,7 @@ public async Task CanAccessStatelessRest() [TestMethod("Can access Stateless GoTrue")] public void CanAccessStatelessGotrue() { - var gotrueOptions = GetAuthOptions(supabaseUrl, null, options); + var gotrueOptions = GetAuthOptions(supabaseUrl, null, options); var client = new Supabase.Gotrue.Client(gotrueOptions); @@ -56,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 index 32386e42..d4c7800e 100644 --- a/SupabaseTests/Stubs/FakeAuthClient.cs +++ b/SupabaseTests/Stubs/FakeAuthClient.cs @@ -13,6 +13,8 @@ internal class FakeAuthClient : IGotrueClient 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) diff --git a/SupabaseTests/Stubs/FakeFunctionsClient.cs b/SupabaseTests/Stubs/FakeFunctionsClient.cs index c057ec97..e63a23f0 100644 --- a/SupabaseTests/Stubs/FakeFunctionsClient.cs +++ b/SupabaseTests/Stubs/FakeFunctionsClient.cs @@ -9,6 +9,8 @@ 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(); diff --git a/SupabaseTests/Stubs/FakeRestClient.cs b/SupabaseTests/Stubs/FakeRestClient.cs index 8e872cb7..c091d537 100644 --- a/SupabaseTests/Stubs/FakeRestClient.cs +++ b/SupabaseTests/Stubs/FakeRestClient.cs @@ -15,6 +15,8 @@ internal class FakeRestClient : IPostgrestClient 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(); diff --git a/SupabaseTests/Stubs/FakeStorageClient.cs b/SupabaseTests/Stubs/FakeStorageClient.cs index 0f04cc0c..d3be41a7 100644 --- a/SupabaseTests/Stubs/FakeStorageClient.cs +++ b/SupabaseTests/Stubs/FakeStorageClient.cs @@ -10,6 +10,7 @@ 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) { 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 563f37b2..8d06da54 160000 --- a/modules/functions-csharp +++ b/modules/functions-csharp @@ -1 +1 @@ -Subproject commit 563f37b2921880d63ffa8a9a2f7c769b2bffc41e +Subproject commit 8d06da54b89677d9f587a1f0998ac708feb5c418 diff --git a/modules/gotrue-csharp b/modules/gotrue-csharp index 7a5ad805..7ad15ba5 160000 --- a/modules/gotrue-csharp +++ b/modules/gotrue-csharp @@ -1 +1 @@ -Subproject commit 7a5ad805e8d7b8f9b9e2709918289e2084fecf8c +Subproject commit 7ad15ba5491a286df674ab755b40a253100ef698 diff --git a/modules/postgrest-csharp b/modules/postgrest-csharp index f9df7ab5..5845d4da 160000 --- a/modules/postgrest-csharp +++ b/modules/postgrest-csharp @@ -1 +1 @@ -Subproject commit f9df7ab55370b7aa08cbf9884d62eac58dc9cd25 +Subproject commit 5845d4da3a47bd8d4e2b07d0fd693996c307e4cd diff --git a/modules/realtime-csharp b/modules/realtime-csharp index 3d35c4e2..ffb1b55b 160000 --- a/modules/realtime-csharp +++ b/modules/realtime-csharp @@ -1 +1 @@ -Subproject commit 3d35c4e2f0c7145c5c50fda76a1cf41d18e8dbda +Subproject commit ffb1b55b7f13de69c70cf080916978fc25c9823a diff --git a/modules/storage-csharp b/modules/storage-csharp index 16346d05..b3fc157d 160000 --- a/modules/storage-csharp +++ b/modules/storage-csharp @@ -1 +1 @@ -Subproject commit 16346d056611dc8fe3674a3bc7ea794fd3ecbcd6 +Subproject commit b3fc157d841df5f52694e758eb40972938006874 From 70433fe2d05810d2b2ec150187a1c54a0b3a346a Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Sat, 12 Nov 2022 18:35:46 -0600 Subject: [PATCH 7/9] - Add a constructor that accepts the clients as arguments. - Fix scope on private variables that are only used at initialization. --- Supabase/Client.cs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/Supabase/Client.cs b/Supabase/Client.cs index 2874fa10..e1c2ac42 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -101,15 +101,19 @@ public IStorageClient Storage private IStorageClient _storage; private string? supabaseKey; - private string authUrl; - private string restUrl; - private string realtimeUrl; - private string storageUrl; - private string functionsUrl; - private string schema; - private SupabaseOptions options; + public Client(IGotrueClient auth, IRealtimeClient realtime, IFunctionsClient functions, IPostgrestClient postgrest, IStorageClient storage, string? supabaseKey, SupabaseOptions options) + { + _auth = auth; + _realtime = realtime; + _functions = functions; + _postgrest = postgrest; + _storage = storage; + this.supabaseKey = supabaseKey; + this.options = options; + } + public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options = null) { @@ -118,15 +122,16 @@ public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options options ??= new SupabaseOptions(); this.options = options; - authUrl = string.Format(options.AuthUrlFormat, supabaseUrl); - restUrl = string.Format(options.RestUrlFormat, supabaseUrl); - realtimeUrl = string.Format(options.RealtimeUrlFormat, supabaseUrl).Replace("http", "ws"); - storageUrl = string.Format(options.StorageUrlFormat, supabaseUrl); - 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('.'); From a750c1fb67e8ddd7ee983de32d617c5a88620b8d Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Sat, 12 Nov 2022 21:11:17 -0600 Subject: [PATCH 8/9] Final adjustments to remove redundant headers call and unused supabaseKey argument --- Supabase/Client.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Supabase/Client.cs b/Supabase/Client.cs index e1c2ac42..d845c25a 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -103,20 +103,18 @@ public IStorageClient Storage private string? supabaseKey; private SupabaseOptions options; - public Client(IGotrueClient auth, IRealtimeClient realtime, IFunctionsClient functions, IPostgrestClient postgrest, IStorageClient storage, string? supabaseKey, SupabaseOptions options) + public Client(IGotrueClient auth, IRealtimeClient realtime, IFunctionsClient functions, IPostgrestClient postgrest, IStorageClient storage, SupabaseOptions options) { _auth = auth; _realtime = realtime; _functions = functions; _postgrest = postgrest; _storage = storage; - this.supabaseKey = supabaseKey; this.options = options; } public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options = null) { - this.supabaseKey = supabaseKey; options ??= new SupabaseOptions(); @@ -205,11 +203,7 @@ private void Auth_StateChanged(object sender, ClientStateChanged e) case AuthState.SignedIn: case AuthState.TokenRefreshed: if (Auth.CurrentSession?.AccessToken != null) - { Realtime.SetAuth(Auth.CurrentSession.AccessToken); - } - _postgrest.Options.Headers = GetAuthHeaders(); - _storage.Headers = GetAuthHeaders(); break; // Remove Realtime Subscriptions on Auth Signout. From 374134fd1502e6bca10c45fc36b8441877665542 Mon Sep 17 00:00:00 2001 From: Joseph Schultz Date: Sat, 12 Nov 2022 21:27:16 -0600 Subject: [PATCH 9/9] Additional comments prior to merge --- Supabase/Client.cs | 24 +++++++++++++++++------ Supabase/DefaultSupabaseSessionHandler.cs | 3 +++ Supabase/StatelessClient.cs | 3 ++- Supabase/SupabaseModel.cs | 3 +-- Supabase/SupabaseOptions.cs | 3 +++ Supabase/SupabaseTable.cs | 6 +++--- Supabase/Util.cs | 19 ------------------ 7 files changed, 30 insertions(+), 31 deletions(-) delete mode 100644 Supabase/Util.cs diff --git a/Supabase/Client.cs b/Supabase/Client.cs index d845c25a..172f872d 100644 --- a/Supabase/Client.cs +++ b/Supabase/Client.cs @@ -5,6 +5,7 @@ using Postgrest.Models; using Postgrest.Responses; using Storage.Interfaces; +using Supabase.Core; using Supabase.Functions.Interfaces; using Supabase.Gotrue; using Supabase.Gotrue.Interfaces; @@ -103,6 +104,15 @@ public IStorageClient Storage private string? supabaseKey; private SupabaseOptions options; + /// + /// Constructor supplied for dependency injection support. + /// + /// + /// + /// + /// + /// + /// public Client(IGotrueClient auth, IRealtimeClient realtime, IFunctionsClient functions, IPostgrestClient postgrest, IStorageClient storage, SupabaseOptions options) { _auth = auth; @@ -113,6 +123,12 @@ public Client(IGotrueClient auth, IRealtimeClient + /// Creates a new Supabase Client. + /// + /// + /// + /// public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options = null) { this.supabaseKey = supabaseKey; @@ -177,12 +193,8 @@ public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options /// - /// Initializes a Supabase Client. + /// 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(); @@ -234,7 +246,7 @@ internal Dictionary GetAuthHeaders() { var headers = new Dictionary(); - headers["X-Client-Info"] = Util.GetAssemblyVersion(); + headers["X-Client-Info"] = Util.GetAssemblyVersion(typeof(Client)); if (supabaseKey != null) { diff --git a/Supabase/DefaultSupabaseSessionHandler.cs b/Supabase/DefaultSupabaseSessionHandler.cs index fcce965c..603373d3 100644 --- a/Supabase/DefaultSupabaseSessionHandler.cs +++ b/Supabase/DefaultSupabaseSessionHandler.cs @@ -4,6 +4,9 @@ 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); diff --git a/Supabase/StatelessClient.cs b/Supabase/StatelessClient.cs index 5b6d7392..42f002d8 100644 --- a/Supabase/StatelessClient.cs +++ b/Supabase/StatelessClient.cs @@ -7,6 +7,7 @@ using Postgrest.Models; using Postgrest.Responses; using Storage.Interfaces; +using Supabase.Core; using Supabase.Extensions; using Supabase.Functions.Interfaces; using Supabase.Gotrue; @@ -141,7 +142,7 @@ internal static Dictionary GetAuthHeaders(string? supabaseKey, S { var headers = new Dictionary(); - headers["X-Client-Info"] = Util.GetAssemblyVersion(); + headers["X-Client-Info"] = Util.GetAssemblyVersion(typeof(Client)); if (supabaseKey != null) { diff --git a/Supabase/SupabaseModel.cs b/Supabase/SupabaseModel.cs index 318fec5e..b36c6c6b 100644 --- a/Supabase/SupabaseModel.cs +++ b/Supabase/SupabaseModel.cs @@ -7,6 +7,5 @@ namespace Supabase { [Obsolete] public abstract class SupabaseModel : BaseModel - { - } + {} } diff --git a/Supabase/SupabaseOptions.cs b/Supabase/SupabaseOptions.cs index 4829fd1d..ccd1bc6d 100644 --- a/Supabase/SupabaseOptions.cs +++ b/Supabase/SupabaseOptions.cs @@ -10,6 +10,9 @@ namespace Supabase /// public class SupabaseOptions { + /// + /// Schema to be used in Postgres / Realtime + /// public string Schema = "public"; /// diff --git a/Supabase/SupabaseTable.cs b/Supabase/SupabaseTable.cs index 1b64b8a7..c2acffe1 100644 --- a/Supabase/SupabaseTable.cs +++ b/Supabase/SupabaseTable.cs @@ -36,13 +36,13 @@ public async Task On(ChannelEventType e, Action(); // In regard to: https://github.com/supabase/supabase-js/pull/270 - var headers = postgrestClient.Options.Headers; - if (headers.ContainsKey("Authorization")) + var headers = postgrestClient?.GetHeaders?.Invoke(); + if (headers != null && headers.ContainsKey("Authorization")) { parameters.Add("user_token", headers["Authorization"].Split(' ')[1]); } - channel = realtimeClient.Channel("realtime", this.schema, TableName, parameters: parameters); + channel = realtimeClient.Channel("realtime", schema, TableName, parameters: parameters); } if (realtimeClient.Socket == null || !realtimeClient.Socket.IsConnected) 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}"; - } - } -}