Skip to content

Commit 8648e6d

Browse files
committed
Add stateless client (re: #7)
1 parent e3dc082 commit 8648e6d

File tree

6 files changed

+224
-7
lines changed

6 files changed

+224
-7
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Supabase.Extensions
6+
{
7+
internal static class DictionaryExtensions
8+
{
9+
// Works in C#3/VS2008:
10+
// Returns a new dictionary of this ... others merged leftward.
11+
// Keeps the type of 'this', which must be default-instantiable.
12+
// Example:
13+
// result = map.MergeLeft(other1, other2, ...)
14+
// From: https://stackoverflow.com/a/2679857/3629438
15+
public static T MergeLeft<T, K, V>(this T me, params IDictionary<K, V>[] others)
16+
where T : IDictionary<K, V>, new()
17+
{
18+
T newMap = new T();
19+
foreach (IDictionary<K, V> src in (new List<IDictionary<K, V>> { me }).Concat(others))
20+
{
21+
foreach (KeyValuePair<K, V> p in src)
22+
{
23+
newMap[p.Key] = p.Value;
24+
}
25+
}
26+
return newMap;
27+
}
28+
29+
}
30+
}

Supabase/StatelessClient.cs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Threading.Tasks;
5+
using Postgrest;
6+
using Postgrest.Models;
7+
using Postgrest.Responses;
8+
using Supabase.Extensions;
9+
using Supabase.Gotrue;
10+
11+
namespace Supabase
12+
{
13+
/// <summary>
14+
/// A Static class representing a Supabase Client.
15+
/// </summary>
16+
public static class StatelessClient
17+
{
18+
public static Gotrue.StatelessClient.StatelessClientOptions GetAuthOptions(string supabaseUrl, string supabaseKey = null, SupabaseOptions options = null)
19+
{
20+
if (options == null)
21+
options = new SupabaseOptions();
22+
23+
var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers);
24+
25+
return new Gotrue.StatelessClient.StatelessClientOptions
26+
{
27+
Url = string.Format(options.AuthUrlFormat, supabaseUrl),
28+
Headers = headers
29+
};
30+
}
31+
32+
public static Postgrest.StatelessClientOptions GetRestOptions(string supabaseUrl, string supabaseKey = null, SupabaseOptions options = null)
33+
{
34+
if (options == null)
35+
options = new SupabaseOptions();
36+
37+
var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers);
38+
39+
return new Postgrest.StatelessClientOptions(string.Format(options.RestUrlFormat, supabaseUrl))
40+
{
41+
Schema = options.Schema,
42+
Headers = headers
43+
};
44+
}
45+
46+
/// <summary>
47+
/// Supabase Storage allows you to manage user-generated content, such as photos or videos.
48+
/// </summary>
49+
/// <param name="supabaseUrl"></param>
50+
/// <param name="supabaseKey"></param>
51+
/// <param name="options"></param>
52+
/// <returns></returns>
53+
public static Storage.Client Storage(string supabaseUrl, string supabaseKey = null, SupabaseOptions options = null)
54+
{
55+
if (options == null)
56+
options = new SupabaseOptions();
57+
58+
var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers);
59+
60+
return new Storage.Client(string.Format(options.StorageUrlFormat, supabaseUrl), headers);
61+
}
62+
63+
/// <summary>
64+
/// Gets the Postgrest client to prepare for a query.
65+
/// </summary>
66+
/// <typeparam name="T"></typeparam>
67+
/// <returns></returns>
68+
public static SupabaseTable<T> From<T>(string supabaseUrl, string supabaseKey, SupabaseOptions options = null) where T : BaseModel, new()
69+
{
70+
if (options == null)
71+
options = new SupabaseOptions();
72+
73+
var headers = GetAuthHeaders(supabaseKey, options).MergeLeft(options.Headers);
74+
75+
76+
return new SupabaseTable<T>(string.Format(options.RestUrlFormat, supabaseUrl), new Postgrest.ClientOptions
77+
{
78+
Headers = headers,
79+
Schema = options.Schema
80+
});
81+
}
82+
83+
/// <summary>
84+
/// Runs a remote procedure.
85+
/// </summary>
86+
/// <param name="procedureName"></param>
87+
/// <param name="parameters"></param>
88+
/// <returns></returns>
89+
public static Task<BaseResponse> Rpc(string supabaseUrl, string supabaseKey, string procedureName, Dictionary<string, object> parameters, SupabaseOptions options = null)
90+
{
91+
if (options == null)
92+
options = new SupabaseOptions();
93+
94+
return Postgrest.StatelessClient.Rpc(procedureName, parameters, GetRestOptions(supabaseUrl, supabaseKey, options));
95+
}
96+
97+
98+
internal static Dictionary<string, string> GetAuthHeaders(string supabaseKey, SupabaseOptions options)
99+
{
100+
var headers = new Dictionary<string, string>();
101+
headers["apiKey"] = supabaseKey;
102+
headers["X-Client-Info"] = Util.GetAssemblyVersion();
103+
104+
// In Regard To: https://github.com/supabase/supabase-csharp/issues/5
105+
if (options.Headers.ContainsKey("Authorization"))
106+
{
107+
headers["Authorization"] = options.Headers["Authorization"];
108+
}
109+
else
110+
{
111+
var bearer = supabaseKey;
112+
headers["Authorization"] = $"Bearer {bearer}";
113+
}
114+
115+
return headers;
116+
}
117+
}
118+
}

Supabase/Supabase.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,17 @@
2525
</PropertyGroup>
2626
<ItemGroup>
2727
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
28-
<PackageReference Include="postgrest-csharp" Version="2.0.2" />
28+
<PackageReference Include="postgrest-csharp" Version="2.0.3" />
2929
<PackageReference Include="realtime-csharp" Version="2.0.4" />
30-
<PackageReference Include="gotrue-csharp" Version="2.2.0" />
30+
<PackageReference Include="gotrue-csharp" Version="2.2.1" />
3131
<PackageReference Include="MimeMapping" Version="1.0.1.37" />
3232
</ItemGroup>
3333
<ItemGroup>
3434
<Folder Include="Storage\" />
35+
<Folder Include="Extensions\" />
3536
</ItemGroup>
3637
<ItemGroup>
3738
<None Remove="MimeMapping" />
39+
<None Remove="Extensions\" />
3840
</ItemGroup>
3941
</Project>

Supabase/SupabaseTable.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ namespace Supabase
1313
private Channel channel;
1414

1515
public SupabaseTable() : base(Client.Instance.RestUrl, new Postgrest.ClientOptions { Headers = Instance.GetAuthHeaders(), Schema = Instance.Schema })
16-
{}
16+
{ }
17+
18+
public SupabaseTable(string restUrl, Postgrest.ClientOptions options) : base(restUrl, options)
19+
{ }
1720

1821
public async Task<Channel> On(ChannelEventType e, Action<object, SocketResponseEventArgs> action)
1922
{
20-
if (channel == null) {
23+
if (channel == null)
24+
{
2125
var parameters = new Dictionary<string, string>();
2226

2327
// In regard to: https://github.com/supabase/supabase-js/pull/270

SupabaseTests/StatelessClient.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using SupabaseTests.Models;
6+
using static Supabase.StatelessClient;
7+
8+
namespace SupabaseTests
9+
{
10+
[TestClass]
11+
public class StatelessClient
12+
{
13+
14+
private string supabaseUrl = "http://localhost";
15+
private Supabase.SupabaseOptions options = new Supabase.SupabaseOptions
16+
{
17+
AuthUrlFormat = "{0}:9999",
18+
RealtimeUrlFormat = "{0}:4000/socket",
19+
RestUrlFormat = "{0}:3000"
20+
};
21+
22+
[TestMethod("Can access Stateless REST")]
23+
public async Task CanAccessStatelessRest()
24+
{
25+
var restOptions = GetRestOptions(supabaseUrl, null, options);
26+
var result1 = await Postgrest.StatelessClient.Table<Channel>(restOptions).Get();
27+
28+
var result2 = await From<Channel>(supabaseUrl, null, options).Get();
29+
30+
Assert.AreEqual(result1.Models.Count, result2.Models.Count);
31+
}
32+
33+
[TestMethod("Can access Stateless GoTrue")]
34+
public void CanAccessStatelessGotrue()
35+
{
36+
var gotrueOptions = GetAuthOptions(supabaseUrl, null, options);
37+
38+
Supabase.Gotrue.StatelessClient.GetApi(gotrueOptions).GetUser("my-user-jwt");
39+
40+
var url = Supabase.Gotrue.StatelessClient.SignIn(Supabase.Gotrue.Client.Provider.Spotify, gotrueOptions);
41+
42+
Assert.IsNotNull(url);
43+
}
44+
45+
[TestMethod("User defined Headers will override internal headers")]
46+
public void CanOverrideInternalHeaders()
47+
{
48+
Supabase.SupabaseOptions options = new Supabase.SupabaseOptions
49+
{
50+
AuthUrlFormat = "{0}:9999",
51+
RealtimeUrlFormat = "{0}:4000/socket",
52+
RestUrlFormat = "{0}:3000",
53+
Headers = new Dictionary<string, string> {
54+
{ "Authorization", "Bearer 123" }
55+
}
56+
};
57+
58+
var gotrueOptions = GetAuthOptions(supabaseUrl, "456", options);
59+
60+
Assert.AreEqual("Bearer 123", gotrueOptions.Headers["Authorization"]);
61+
}
62+
}
63+
}

SupabaseTests/SupabaseTests.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
12-
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
13-
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
12+
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
13+
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
1414
<PackageReference Include="coverlet.collector" Version="3.1.0"><IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1515
<PrivateAssets>all</PrivateAssets>
1616
</PackageReference>

0 commit comments

Comments
 (0)