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