Skip to content

Commit

Permalink
Add stateless client (re: #7)
Browse files Browse the repository at this point in the history
  • Loading branch information
acupofjose committed Nov 26, 2021
1 parent e3dc082 commit 8648e6d
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 7 deletions.
30 changes: 30 additions & 0 deletions Supabase/Extensions/DictionaryExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Supabase.Extensions
{
internal static class DictionaryExtensions
{
// Works in C#3/VS2008:
// Returns a new dictionary of this ... others merged leftward.
// Keeps the type of 'this', which must be default-instantiable.
// Example:
// result = map.MergeLeft(other1, other2, ...)
// From: https://stackoverflow.com/a/2679857/3629438
public static T MergeLeft<T, K, V>(this T me, params IDictionary<K, V>[] others)
where T : IDictionary<K, V>, new()
{
T newMap = new T();
foreach (IDictionary<K, V> src in (new List<IDictionary<K, V>> { me }).Concat(others))
{
foreach (KeyValuePair<K, V> p in src)
{
newMap[p.Key] = p.Value;
}
}
return newMap;
}

}
}
118 changes: 118 additions & 0 deletions Supabase/StatelessClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Postgrest;
using Postgrest.Models;
using Postgrest.Responses;
using Supabase.Extensions;
using Supabase.Gotrue;

namespace Supabase
{
/// <summary>
/// A Static class representing a Supabase Client.
/// </summary>
public static class StatelessClient
{
public static Gotrue.StatelessClient.StatelessClientOptions GetAuthOptions(string supabaseUrl, string supabaseKey = null, SupabaseOptions options = null)
{
if (options == null)
options = new SupabaseOptions();

var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers);

return new Gotrue.StatelessClient.StatelessClientOptions
{
Url = string.Format(options.AuthUrlFormat, supabaseUrl),
Headers = headers
};
}

public static Postgrest.StatelessClientOptions GetRestOptions(string supabaseUrl, string supabaseKey = null, SupabaseOptions options = null)
{
if (options == null)
options = new SupabaseOptions();

var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers);

return new Postgrest.StatelessClientOptions(string.Format(options.RestUrlFormat, supabaseUrl))
{
Schema = options.Schema,
Headers = headers
};
}

/// <summary>
/// Supabase Storage allows you to manage user-generated content, such as photos or videos.
/// </summary>
/// <param name="supabaseUrl"></param>
/// <param name="supabaseKey"></param>
/// <param name="options"></param>
/// <returns></returns>
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);

return new Storage.Client(string.Format(options.StorageUrlFormat, supabaseUrl), headers);
}

/// <summary>
/// Gets the Postgrest client to prepare for a query.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static SupabaseTable<T> From<T>(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);


return new SupabaseTable<T>(string.Format(options.RestUrlFormat, supabaseUrl), new Postgrest.ClientOptions
{
Headers = headers,
Schema = options.Schema
});
}

/// <summary>
/// Runs a remote procedure.
/// </summary>
/// <param name="procedureName"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public static Task<BaseResponse> Rpc(string supabaseUrl, string supabaseKey, string procedureName, Dictionary<string, object> parameters, SupabaseOptions options = null)
{
if (options == null)
options = new SupabaseOptions();

return Postgrest.StatelessClient.Rpc(procedureName, parameters, GetRestOptions(supabaseUrl, supabaseKey, options));
}


internal static Dictionary<string, string> GetAuthHeaders(string supabaseKey, SupabaseOptions options)
{
var headers = new Dictionary<string, string>();
headers["apiKey"] = supabaseKey;
headers["X-Client-Info"] = Util.GetAssemblyVersion();

// In Regard To: https://github.com/supabase/supabase-csharp/issues/5
if (options.Headers.ContainsKey("Authorization"))
{
headers["Authorization"] = options.Headers["Authorization"];
}
else
{
var bearer = supabaseKey;
headers["Authorization"] = $"Bearer {bearer}";
}

return headers;
}
}
}
6 changes: 4 additions & 2 deletions Supabase/Supabase.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="postgrest-csharp" Version="2.0.2" />
<PackageReference Include="postgrest-csharp" Version="2.0.3" />
<PackageReference Include="realtime-csharp" Version="2.0.4" />
<PackageReference Include="gotrue-csharp" Version="2.2.0" />
<PackageReference Include="gotrue-csharp" Version="2.2.1" />
<PackageReference Include="MimeMapping" Version="1.0.1.37" />
</ItemGroup>
<ItemGroup>
<Folder Include="Storage\" />
<Folder Include="Extensions\" />
</ItemGroup>
<ItemGroup>
<None Remove="MimeMapping" />
<None Remove="Extensions\" />
</ItemGroup>
</Project>
8 changes: 6 additions & 2 deletions Supabase/SupabaseTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ namespace Supabase
private Channel channel;

public SupabaseTable() : base(Client.Instance.RestUrl, new Postgrest.ClientOptions { Headers = Instance.GetAuthHeaders(), Schema = Instance.Schema })
{}
{ }

public SupabaseTable(string restUrl, Postgrest.ClientOptions options) : base(restUrl, options)
{ }

public async Task<Channel> On(ChannelEventType e, Action<object, SocketResponseEventArgs> action)
{
if (channel == null) {
if (channel == null)
{
var parameters = new Dictionary<string, string>();

// In regard to: https://github.com/supabase/supabase-js/pull/270
Expand Down
63 changes: 63 additions & 0 deletions SupabaseTests/StatelessClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SupabaseTests.Models;
using static Supabase.StatelessClient;

namespace SupabaseTests
{
[TestClass]
public class StatelessClient
{

private string supabaseUrl = "http://localhost";
private Supabase.SupabaseOptions options = new Supabase.SupabaseOptions
{
AuthUrlFormat = "{0}:9999",
RealtimeUrlFormat = "{0}:4000/socket",
RestUrlFormat = "{0}:3000"
};

[TestMethod("Can access Stateless REST")]
public async Task CanAccessStatelessRest()
{
var restOptions = GetRestOptions(supabaseUrl, null, options);
var result1 = await Postgrest.StatelessClient.Table<Channel>(restOptions).Get();

var result2 = await From<Channel>(supabaseUrl, null, options).Get();

Assert.AreEqual(result1.Models.Count, result2.Models.Count);
}

[TestMethod("Can access Stateless GoTrue")]
public void CanAccessStatelessGotrue()
{
var gotrueOptions = GetAuthOptions(supabaseUrl, null, options);

Supabase.Gotrue.StatelessClient.GetApi(gotrueOptions).GetUser("my-user-jwt");

var url = Supabase.Gotrue.StatelessClient.SignIn(Supabase.Gotrue.Client.Provider.Spotify, gotrueOptions);

Assert.IsNotNull(url);
}

[TestMethod("User defined Headers will override internal headers")]
public void CanOverrideInternalHeaders()
{
Supabase.SupabaseOptions options = new Supabase.SupabaseOptions
{
AuthUrlFormat = "{0}:9999",
RealtimeUrlFormat = "{0}:4000/socket",
RestUrlFormat = "{0}:3000",
Headers = new Dictionary<string, string> {
{ "Authorization", "Bearer 123" }
}
};

var gotrueOptions = GetAuthOptions(supabaseUrl, "456", options);

Assert.AreEqual("Bearer 123", gotrueOptions.Headers["Authorization"]);
}
}
}
6 changes: 3 additions & 3 deletions SupabaseTests/SupabaseTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="coverlet.collector" Version="3.1.0"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down

0 comments on commit 8648e6d

Please sign in to comment.