From 8b3151a4670947b8b94cda99845028081af18ffa Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 8 Apr 2025 10:31:01 +0200 Subject: [PATCH 1/8] Add Supabase Connector integration and configuration support --- .gitignore | 2 + demos/CommandLine/.env.template | 3 + demos/CommandLine/CommandLine.csproj | 2 + demos/CommandLine/CommandLine.sln | 24 +++ demos/CommandLine/Demo.cs | 13 +- .../Helpers/SupabasePatchHelper.cs | 34 ++++ demos/CommandLine/Models/List.cs | 25 +++ demos/CommandLine/Models/Todos.cs | 41 +++++ demos/CommandLine/README.md | 26 ++- demos/CommandLine/SupabaseConnector.cs | 167 ++++++++++++++++++ demos/CommandLine/Utils/Config.cs | 23 +++ user_id.txt | 1 + 12 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 demos/CommandLine/.env.template create mode 100644 demos/CommandLine/CommandLine.sln create mode 100644 demos/CommandLine/Helpers/SupabasePatchHelper.cs create mode 100644 demos/CommandLine/Models/List.cs create mode 100644 demos/CommandLine/Models/Todos.cs create mode 100644 demos/CommandLine/SupabaseConnector.cs create mode 100644 demos/CommandLine/Utils/Config.cs create mode 100644 user_id.txt diff --git a/.gitignore b/.gitignore index 20bf598..05654a3 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ TestResults/ *.dylib *.dll *.so + +.env diff --git a/demos/CommandLine/.env.template b/demos/CommandLine/.env.template new file mode 100644 index 0000000..b60b823 --- /dev/null +++ b/demos/CommandLine/.env.template @@ -0,0 +1,3 @@ +SUPABASE_URL=your-supabase-url +SUPABASE_ANON_KEY=your_anon_key_here +POWERSYNC_URL=your-powersync-url diff --git a/demos/CommandLine/CommandLine.csproj b/demos/CommandLine/CommandLine.csproj index f33a0ab..920769d 100644 --- a/demos/CommandLine/CommandLine.csproj +++ b/demos/CommandLine/CommandLine.csproj @@ -13,8 +13,10 @@ + + diff --git a/demos/CommandLine/CommandLine.sln b/demos/CommandLine/CommandLine.sln new file mode 100644 index 0000000..a24ff89 --- /dev/null +++ b/demos/CommandLine/CommandLine.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine", "CommandLine.csproj", "{6BB9F16E-3825-DE76-1286-9E5E2406710D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6BB9F16E-3825-DE76-1286-9E5E2406710D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BB9F16E-3825-DE76-1286-9E5E2406710D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BB9F16E-3825-DE76-1286-9E5E2406710D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BB9F16E-3825-DE76-1286-9E5E2406710D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A5588511-5909-4F05-80EB-09A56805607C} + EndGlobalSection +EndGlobal diff --git a/demos/CommandLine/Demo.cs b/demos/CommandLine/Demo.cs index dacb958..e85b066 100644 --- a/demos/CommandLine/Demo.cs +++ b/demos/CommandLine/Demo.cs @@ -1,5 +1,6 @@ namespace CommandLine; +using CommandLine.Utils; using PowerSync.Common.Client; using Spectre.Console; @@ -16,7 +17,11 @@ static async Task Main() }); await db.Init(); - var connector = new NodeConnector(); + //var connector = new NodeConnector(); + var config = new SupabaseConfig(); + var connector = new SupabaseConnector(config); + + await connector.Login("dean@journeyapps.com", "Dean1998"); var table = new Table() .AddColumn("id") @@ -47,6 +52,12 @@ static async Task Main() } }); + // await db.Execute("insert into lists (id, name, owner_id, created_at) values (uuid(), 'New User33', ?, datetime())", [connector.UserId]); + + await db.Execute( + "UPDATE lists SET name = ?, created_at = datetime() WHERE owner_id = ? and id = ?", ["update CHCHCHCHCH" , connector.UserId, "0bf55412-d35b-4814-ade9-daea4865df96"] + ); + var _ = Task.Run(async () => { while (running) diff --git a/demos/CommandLine/Helpers/SupabasePatchHelper.cs b/demos/CommandLine/Helpers/SupabasePatchHelper.cs new file mode 100644 index 0000000..8cdef43 --- /dev/null +++ b/demos/CommandLine/Helpers/SupabasePatchHelper.cs @@ -0,0 +1,34 @@ +namespace CommandLine.Helpers; + +using System.Linq.Expressions; +using Newtonsoft.Json; +using Supabase.Postgrest.Interfaces; +using Supabase.Postgrest.Models; + +public static class SupabasePatchHelper +{ + public static IPostgrestTable ApplySet( + IPostgrestTable table, + string jsonPropertyName, + object value + ) where T : BaseModel, new() + { + // Find the property that matches the JsonProperty name + var property = typeof(T) + .GetProperties() + .FirstOrDefault(p => + p.GetCustomAttributes(typeof(JsonPropertyAttribute), true) + .FirstOrDefault() is JsonPropertyAttribute attr && + attr.PropertyName == jsonPropertyName); + + if (property == null) + throw new ArgumentException($"'{jsonPropertyName}' is not a valid property on type '{typeof(T).Name}'"); + + var parameter = Expression.Parameter(typeof(T), "x"); + var propertyAccess = Expression.Property(parameter, property.Name); + var converted = Expression.Convert(propertyAccess, typeof(object)); + var lambda = Expression.Lambda>(converted, parameter); + + return table.Set(lambda, value); + } +} \ No newline at end of file diff --git a/demos/CommandLine/Models/List.cs b/demos/CommandLine/Models/List.cs new file mode 100644 index 0000000..4164c58 --- /dev/null +++ b/demos/CommandLine/Models/List.cs @@ -0,0 +1,25 @@ +namespace CommandLine.Models; + +using Newtonsoft.Json; +using Supabase.Postgrest.Attributes; +using Supabase.Postgrest.Models; + +[Table("lists")] +class List : BaseModel +{ + [PrimaryKey("id")] + [JsonProperty("id")] + public string Id { get; set; } + + [Column("created_at")] + [JsonProperty("created_at")] + public string CreatedAt { get; set; } + + [Column("name")] + [JsonProperty("name")] + public string Name { get; set; } + + [Column("owner_id")] + [JsonProperty("owner_id")] + public string OwnerId { get; set; } +} diff --git a/demos/CommandLine/Models/Todos.cs b/demos/CommandLine/Models/Todos.cs new file mode 100644 index 0000000..fd7eb19 --- /dev/null +++ b/demos/CommandLine/Models/Todos.cs @@ -0,0 +1,41 @@ +namespace CommandLine.Models; + +using Newtonsoft.Json; +using Supabase.Postgrest.Attributes; +using Supabase.Postgrest.Models; + +[Table("todos")] +class Todo : BaseModel +{ + [PrimaryKey("id")] + [JsonProperty("id")] + public string Id { get; set; } + + [Column("list_id")] + [JsonProperty("list_id")] + public string ListId { get; set; } + + [Column("created_at")] + [JsonProperty("created_at")] + public string CreatedAt { get; set; } + + [Column("completed_at")] + [JsonProperty("completed_at")] + public string CompletedAt { get; set; } + + [Column("description")] + [JsonProperty("description")] + public string Description { get; set; } + + [Column("created_by")] + [JsonProperty("created_by")] + public string CreatedBy { get; set; } + + [Column("completed_by")] + [JsonProperty("completed_by")] + public string CompletedBy { get; set; } + + [Column("completed")] + [JsonProperty("completed")] + public int Completed { get; set; } +} diff --git a/demos/CommandLine/README.md b/demos/CommandLine/README.md index 0f9fb70..dffc34c 100644 --- a/demos/CommandLine/README.md +++ b/demos/CommandLine/README.md @@ -1,4 +1,4 @@ -# PowerSync CLI demo app +# PowerSync CLI Demo App This demo features a CLI-based table view that stays *live* using a *watch query*, ensuring the data updates in real time as changes occur. To run this demo, you need to have one of our Node.js self-host demos ([Postgres](https://github.com/powersync-ja/self-host-demo/tree/main/demos/nodejs) | [MongoDB](https://github.com/powersync-ja/self-host-demo/tree/main/demos/nodejs-mongodb) | [MySQL](https://github.com/powersync-ja/self-host-demo/tree/main/demos/nodejs-mysql)) running, as it provides the PowerSync server that this CLI's PowerSync SDK connects to. @@ -9,6 +9,28 @@ Changes made to the backend's source DB or to the self-hosted web UI will be syn This essentially uses anonymous authentication. A random user ID is generated and stored in local storage. The backend returns a valid token which is not linked to a specific user. All data is synced to all users. +## Connection Options + +By default, this demo uses the NodeConnector for connecting to the PowerSync server. However, you can swap this out with the SupabaseConnector if needed: + +1. Copy the `.env.template` file to a new `.env` file: + ```bash + # On Linux/macOS + cp .env.template .env + + # On Windows + copy .env.template .env + ``` + +2. Replace the necessary fields in the `.env` file with your Supabase and PowerSync credentials: + ``` + SUPABASE_URL=your_supabase_url + SUPABASE_ANON_KEY=your_supabase_anon_key + POWERSYNC_URL=your_powersync_url + ``` + +3. Update your connector configuration to use SupabaseConnector instead of NodeConnector + ## Getting Started In the repo root, run the following to download the PowerSync extension: @@ -29,4 +51,4 @@ To run the Command-Line interface: ```bash dotnet run Demo -``` +``` \ No newline at end of file diff --git a/demos/CommandLine/SupabaseConnector.cs b/demos/CommandLine/SupabaseConnector.cs new file mode 100644 index 0000000..be28507 --- /dev/null +++ b/demos/CommandLine/SupabaseConnector.cs @@ -0,0 +1,167 @@ +namespace CommandLine; + +using CommandLine.Helpers; +using CommandLine.Models; +using CommandLine.Utils; +using Newtonsoft.Json; +using PowerSync.Common.Client; +using PowerSync.Common.Client.Connection; +using PowerSync.Common.DB.Crud; +using Supabase; +using Supabase.Gotrue; +using Supabase.Postgrest.Exceptions; +using Supabase.Postgrest.Interfaces; + +public class SupabaseConnector : IPowerSyncBackendConnector +{ + private readonly Supabase.Client _supabase; + private readonly SupabaseConfig _config; + private Session? _currentSession; + + public Session? CurrentSession + { + get => _currentSession; + set + { + _currentSession = value; + + if (_currentSession?.User?.Id != null) + { + UserId = _currentSession.User.Id; + } + } + } + + public string UserId { get; private set; } = ""; + + public bool Ready { get; private set; } + + public SupabaseConnector(SupabaseConfig config) + { + _config = config; + _supabase = new Supabase.Client(config.SupabaseUrl, config.SupabaseAnonKey, new SupabaseOptions + { + AutoConnectRealtime = true + }); + + _ = _supabase.InitializeAsync(); + } + + public async Task Login(string email, string password) + { + var response = await _supabase.Auth.SignInWithPassword(email, password); + if (response?.User == null || response.AccessToken == null) + { + throw new Exception("Login failed."); + } + + CurrentSession = response; + } + + public Task FetchCredentials() + { + PowerSyncCredentials? credentials = null; + + var sessionResponse = _supabase.Auth.CurrentSession; + if (sessionResponse?.AccessToken != null) + { + credentials = new PowerSyncCredentials(_config.PowerSyncUrl, sessionResponse.AccessToken); + } + + return Task.FromResult(credentials); + } + + public async Task UploadData(IPowerSyncDatabase database) + { + var transaction = await database.GetNextCrudTransaction(); + if (transaction == null) return; + + try + { + foreach (var op in transaction.Crud) + { + switch (op.Op) + { + case UpdateType.PUT: + if (op.Table.ToLower().Trim() == "lists") + { + var model = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(op.OpData)) ?? throw new InvalidOperationException("Model is null."); + model.Id = op.Id; + + await _supabase.From().Upsert(model); + } + else if (op.Table.ToLower().Trim() == "todos") + { + var model = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(op.OpData)) ?? throw new InvalidOperationException("Model is null."); + model.Id = op.Id; + + await _supabase.From().Upsert(model); + } + break; + + case UpdateType.PATCH: + if (op.OpData is null || op.OpData.Count == 0) + { + Console.WriteLine("PATCH skipped: No data to update."); + break; + } + + if (op.Table.ToLower().Trim() == "lists") + { + IPostgrestTable updateQuery = _supabase + .From() + .Where(x => x.Id == op.Id); + + foreach (var kvp in op.OpData) + { + updateQuery = SupabasePatchHelper.ApplySet(updateQuery, kvp.Key, kvp.Value); + } + + _ = await updateQuery.Update(); + } + else if (op.Table.ToLower().Trim() == "todos") + { + IPostgrestTable updateQuery = _supabase + .From() + .Where(x => x.Id == op.Id); + + foreach (var kvp in op.OpData) + { + updateQuery = SupabasePatchHelper.ApplySet(updateQuery, kvp.Key, kvp.Value); + } + + _ = await updateQuery.Update(); + } + break; + + case UpdateType.DELETE: + if (op.Table.ToLower().Trim() == "lists") + { + await _supabase + .From() + .Where(x => x.Id == op.Id) + .Delete(); + } + else if (op.Table.ToLower().Trim() == "todos") + { + await _supabase + .From() + .Where(x => x.Id == op.Id) + .Delete(); + } + break; + + default: + throw new InvalidOperationException("Unknown operation type."); + } + } + + await transaction.Complete(); + } + catch (PostgrestException ex) + { + Console.WriteLine($"Error during upload: {ex.Message}"); + throw; + } + } +} \ No newline at end of file diff --git a/demos/CommandLine/Utils/Config.cs b/demos/CommandLine/Utils/Config.cs new file mode 100644 index 0000000..2068fdb --- /dev/null +++ b/demos/CommandLine/Utils/Config.cs @@ -0,0 +1,23 @@ +namespace CommandLine.Utils; + +using dotenv.net; + +public class SupabaseConfig +{ + public string SupabaseUrl { get; set; } + public string SupabaseAnonKey { get; set; } + public string PowerSyncUrl { get; set; } + + public SupabaseConfig() + { + DotEnv.Load(); + Console.WriteLine($"Current directory: {Directory.GetCurrentDirectory()}"); + // Retrieve the environment variables + SupabaseUrl = Environment.GetEnvironmentVariable("SUPABASE_URL") + ?? throw new InvalidOperationException("SUPABASE_URL environment variable is not set."); + SupabaseAnonKey = Environment.GetEnvironmentVariable("SUPABASE_ANON_KEY") + ?? throw new InvalidOperationException("SUPABASE_ANON_KEY environment variable is not set."); + PowerSyncUrl = Environment.GetEnvironmentVariable("POWERSYNC_URL") + ?? throw new InvalidOperationException("POWERSYNC_URL environment variable is not set."); + } +} \ No newline at end of file diff --git a/user_id.txt b/user_id.txt new file mode 100644 index 0000000..2d4a165 --- /dev/null +++ b/user_id.txt @@ -0,0 +1 @@ +72394969-fa2f-476b-822b-b2d5ba89acca \ No newline at end of file From dc58656a62e6fe4d3ad766171f702522694825b1 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 8 Apr 2025 11:16:45 +0200 Subject: [PATCH 2/8] Add configuration options for Supabase and Node connectors --- demos/CommandLine/.env.template | 6 +++++ demos/CommandLine/Demo.cs | 33 ++++++++++++++++++++------ demos/CommandLine/NodeConnector.cs | 8 +++---- demos/CommandLine/SupabaseConnector.cs | 4 ++-- demos/CommandLine/Utils/Config.cs | 27 +++++++++++++++++---- 5 files changed, 61 insertions(+), 17 deletions(-) diff --git a/demos/CommandLine/.env.template b/demos/CommandLine/.env.template index b60b823..0747ad8 100644 --- a/demos/CommandLine/.env.template +++ b/demos/CommandLine/.env.template @@ -1,3 +1,9 @@ SUPABASE_URL=your-supabase-url SUPABASE_ANON_KEY=your_anon_key_here POWERSYNC_URL=your-powersync-url +BACKEND_URL=your-backend-url +SUPABASE_USERNAME=your-supabase-username +SUPABASE_PASSWORD=your-supabase-password +# Set to true if you want to use Supabase as the backend +# Set to false if you want to use the Powersync backend +USE_SUPABASE=true \ No newline at end of file diff --git a/demos/CommandLine/Demo.cs b/demos/CommandLine/Demo.cs index e85b066..bca0867 100644 --- a/demos/CommandLine/Demo.cs +++ b/demos/CommandLine/Demo.cs @@ -2,11 +2,11 @@ using CommandLine.Utils; using PowerSync.Common.Client; +using PowerSync.Common.Client.Connection; using Spectre.Console; class Demo { - private record ListResult(string id, string name, string owner_id, string created_at); static async Task Main() { @@ -17,11 +17,30 @@ static async Task Main() }); await db.Init(); - //var connector = new NodeConnector(); - var config = new SupabaseConfig(); - var connector = new SupabaseConnector(config); + var config = new Config(); + + IPowerSyncBackendConnector connector; + + string connectorUserId = ""; + + if (config.UseSupabase) + { + var supabaseConnector = new SupabaseConnector(config); + + await supabaseConnector.Login(config.SupabaseUsername, config.SupabasePassword); + + connectorUserId = supabaseConnector.UserId; + + connector = supabaseConnector; + } + else + { + var nodeConnector = new NodeConnector(config); + + connectorUserId = nodeConnector.UserId; - await connector.Login("dean@journeyapps.com", "Dean1998"); + connector = nodeConnector; + } var table = new Table() .AddColumn("id") @@ -55,7 +74,7 @@ static async Task Main() // await db.Execute("insert into lists (id, name, owner_id, created_at) values (uuid(), 'New User33', ?, datetime())", [connector.UserId]); await db.Execute( - "UPDATE lists SET name = ?, created_at = datetime() WHERE owner_id = ? and id = ?", ["update CHCHCHCHCH" , connector.UserId, "0bf55412-d35b-4814-ade9-daea4865df96"] + "UPDATE lists SET name = ?, created_at = datetime() WHERE owner_id = ? and id = ?", ["update CHCHCHCHCH", connectorUserId, "0bf55412-d35b-4814-ade9-daea4865df96"] ); var _ = Task.Run(async () => @@ -71,7 +90,7 @@ await db.Execute( } else if (key.Key == ConsoleKey.Enter) { - await db.Execute("insert into lists (id, name, owner_id, created_at) values (uuid(), 'New User', ?, datetime())", [connector.UserId]); + await db.Execute("insert into lists (id, name, owner_id, created_at) values (uuid(), 'New User', ?, datetime())", [connectorUserId]); } else if (key.Key == ConsoleKey.Backspace) { diff --git a/demos/CommandLine/NodeConnector.cs b/demos/CommandLine/NodeConnector.cs index b7c5dfc..043a6e5 100644 --- a/demos/CommandLine/NodeConnector.cs +++ b/demos/CommandLine/NodeConnector.cs @@ -10,7 +10,7 @@ using PowerSync.Common.Client; using PowerSync.Common.Client.Connection; using PowerSync.Common.DB.Crud; - +using CommandLine.Utils; public class NodeConnector : IPowerSyncBackendConnector { @@ -22,15 +22,15 @@ public class NodeConnector : IPowerSyncBackendConnector public string UserId { get; private set; } private string? clientId; - public NodeConnector() + public NodeConnector(Config config) { _httpClient = new HttpClient(); // Load or generate User ID UserId = LoadOrGenerateUserId(); - BackendUrl = "http://localhost:6060"; - PowerSyncUrl = "http://localhost:8080"; + BackendUrl = config.BackendUrl; + PowerSyncUrl = config.PowerSyncUrl; clientId = null; } diff --git a/demos/CommandLine/SupabaseConnector.cs b/demos/CommandLine/SupabaseConnector.cs index be28507..15a60ae 100644 --- a/demos/CommandLine/SupabaseConnector.cs +++ b/demos/CommandLine/SupabaseConnector.cs @@ -15,7 +15,7 @@ namespace CommandLine; public class SupabaseConnector : IPowerSyncBackendConnector { private readonly Supabase.Client _supabase; - private readonly SupabaseConfig _config; + private readonly Config _config; private Session? _currentSession; public Session? CurrentSession @@ -36,7 +36,7 @@ public Session? CurrentSession public bool Ready { get; private set; } - public SupabaseConnector(SupabaseConfig config) + public SupabaseConnector(Config config) { _config = config; _supabase = new Supabase.Client(config.SupabaseUrl, config.SupabaseAnonKey, new SupabaseOptions diff --git a/demos/CommandLine/Utils/Config.cs b/demos/CommandLine/Utils/Config.cs index 2068fdb..604ce42 100644 --- a/demos/CommandLine/Utils/Config.cs +++ b/demos/CommandLine/Utils/Config.cs @@ -1,17 +1,22 @@ -namespace CommandLine.Utils; - using dotenv.net; -public class SupabaseConfig +namespace CommandLine.Utils; + +public class Config { public string SupabaseUrl { get; set; } public string SupabaseAnonKey { get; set; } public string PowerSyncUrl { get; set; } + public string BackendUrl { get; set; } + public string SupabaseUsername { get; set; } + public string SupabasePassword { get; set; } + public bool UseSupabase { get; set; } - public SupabaseConfig() + public Config() { DotEnv.Load(); Console.WriteLine($"Current directory: {Directory.GetCurrentDirectory()}"); + // Retrieve the environment variables SupabaseUrl = Environment.GetEnvironmentVariable("SUPABASE_URL") ?? throw new InvalidOperationException("SUPABASE_URL environment variable is not set."); @@ -19,5 +24,19 @@ public SupabaseConfig() ?? throw new InvalidOperationException("SUPABASE_ANON_KEY environment variable is not set."); PowerSyncUrl = Environment.GetEnvironmentVariable("POWERSYNC_URL") ?? throw new InvalidOperationException("POWERSYNC_URL environment variable is not set."); + BackendUrl = Environment.GetEnvironmentVariable("BACKEND_URL") + ?? throw new InvalidOperationException("BACKEND_URL environment variable is not set."); + SupabaseUsername = Environment.GetEnvironmentVariable("SUPABASE_USERNAME") + ?? throw new InvalidOperationException("SUPABASE_USERNAME environment variable is not set."); + SupabasePassword = Environment.GetEnvironmentVariable("SUPABASE_PASSWORD") + ?? throw new InvalidOperationException("SUPABASE_PASSWORD environment variable is not set."); + + // Parse boolean value + string useSupabaseStr = Environment.GetEnvironmentVariable("USE_SUPABASE") ?? "false"; + if (!bool.TryParse(useSupabaseStr, out bool useSupabase)) + { + throw new InvalidOperationException("USE_SUPABASE environment variable is not a valid boolean."); + } + UseSupabase = useSupabase; } } \ No newline at end of file From cc9eb2694ef8ec12af1a2800666e1337a6858a8f Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 8 Apr 2025 11:22:20 +0200 Subject: [PATCH 3/8] Added comments to the SupabasePatchHelper and PATCH Event in SupabaseConnector --- .../Helpers/SupabasePatchHelper.cs | 28 +++++++++++-------- demos/CommandLine/SupabaseConnector.cs | 12 ++++++-- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/demos/CommandLine/Helpers/SupabasePatchHelper.cs b/demos/CommandLine/Helpers/SupabasePatchHelper.cs index 8cdef43..47b46a8 100644 --- a/demos/CommandLine/Helpers/SupabasePatchHelper.cs +++ b/demos/CommandLine/Helpers/SupabasePatchHelper.cs @@ -7,28 +7,32 @@ namespace CommandLine.Helpers; public static class SupabasePatchHelper { + // Applies a "SET" operation to the table, setting the value of a specific property. public static IPostgrestTable ApplySet( - IPostgrestTable table, - string jsonPropertyName, - object value - ) where T : BaseModel, new() + IPostgrestTable table, // The table to apply the operation to + string jsonPropertyName, // The name of the JSON property to update + object value // The new value to set for the property + ) where T : BaseModel, new() // Ensures T is a subclass of BaseModel with a parameterless constructor { - // Find the property that matches the JsonProperty name + // Find the property on the model that matches the JSON property name var property = typeof(T) - .GetProperties() + .GetProperties() // Get all properties of the model type .FirstOrDefault(p => + // Check if the property has a JsonPropertyAttribute p.GetCustomAttributes(typeof(JsonPropertyAttribute), true) .FirstOrDefault() is JsonPropertyAttribute attr && - attr.PropertyName == jsonPropertyName); + attr.PropertyName == jsonPropertyName); // Check if the JSON property name matches if (property == null) throw new ArgumentException($"'{jsonPropertyName}' is not a valid property on type '{typeof(T).Name}'"); - var parameter = Expression.Parameter(typeof(T), "x"); - var propertyAccess = Expression.Property(parameter, property.Name); - var converted = Expression.Convert(propertyAccess, typeof(object)); - var lambda = Expression.Lambda>(converted, parameter); + // Create an expression to access the specified property on the model + var parameter = Expression.Parameter(typeof(T), "x"); // Define a parameter for the expression + var propertyAccess = Expression.Property(parameter, property.Name); // Access the property + var converted = Expression.Convert(propertyAccess, typeof(object)); // Convert the value to object type + var lambda = Expression.Lambda>(converted, parameter); // Create a lambda expression for the property + // Apply the "SET" operation to the table using the lambda expression return table.Set(lambda, value); } -} \ No newline at end of file +} diff --git a/demos/CommandLine/SupabaseConnector.cs b/demos/CommandLine/SupabaseConnector.cs index 15a60ae..ce02d0b 100644 --- a/demos/CommandLine/SupabaseConnector.cs +++ b/demos/CommandLine/SupabaseConnector.cs @@ -108,12 +108,16 @@ public async Task UploadData(IPowerSyncDatabase database) if (op.Table.ToLower().Trim() == "lists") { + // Create an update query for the 'Todo' table where the 'Id' matches 'op.Id' IPostgrestTable updateQuery = _supabase .From() .Where(x => x.Id == op.Id); + // Loop through each key-value pair in the operation data (op.OpData) to apply updates dynamically foreach (var kvp in op.OpData) { + // Apply the "SET" operation for each key-value pair. + // The key represents the JSON property name and the value is the new value to be set updateQuery = SupabasePatchHelper.ApplySet(updateQuery, kvp.Key, kvp.Value); } @@ -121,12 +125,16 @@ public async Task UploadData(IPowerSyncDatabase database) } else if (op.Table.ToLower().Trim() == "todos") { + // Create an update query for the 'Todo' table where the 'Id' matches 'op.Id' IPostgrestTable updateQuery = _supabase - .From() - .Where(x => x.Id == op.Id); + .From() + .Where(x => x.Id == op.Id); + // Loop through each key-value pair in the operation data (op.OpData) to apply updates dynamically foreach (var kvp in op.OpData) { + // Apply the "SET" operation for each key-value pair. + // The key represents the JSON property name and the value is the new value to be set updateQuery = SupabasePatchHelper.ApplySet(updateQuery, kvp.Key, kvp.Value); } From da2aec22a87beb3f4ac3c4ee9ef01c5651fe120a Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 8 Apr 2025 11:25:00 +0200 Subject: [PATCH 4/8] Updates README to include all .env values --- demos/CommandLine/README.md | 12 +++++++++--- demos/CommandLine/Utils/Config.cs | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/demos/CommandLine/README.md b/demos/CommandLine/README.md index dffc34c..c2da674 100644 --- a/demos/CommandLine/README.md +++ b/demos/CommandLine/README.md @@ -24,9 +24,15 @@ By default, this demo uses the NodeConnector for connecting to the PowerSync ser 2. Replace the necessary fields in the `.env` file with your Supabase and PowerSync credentials: ``` - SUPABASE_URL=your_supabase_url - SUPABASE_ANON_KEY=your_supabase_anon_key - POWERSYNC_URL=your_powersync_url + SUPABASE_URL=your-supabase-url + SUPABASE_ANON_KEY=your_anon_key_here + POWERSYNC_URL=your-powersync-url + BACKEND_URL=your-backend-url + SUPABASE_USERNAME=your-supabase-username + SUPABASE_PASSWORD=your-supabase-password + # Set to true if you want to use Supabase as the backend + # Set to false if you want to use the Powersync backend + USE_SUPABASE=true ``` 3. Update your connector configuration to use SupabaseConnector instead of NodeConnector diff --git a/demos/CommandLine/Utils/Config.cs b/demos/CommandLine/Utils/Config.cs index 604ce42..2ab3713 100644 --- a/demos/CommandLine/Utils/Config.cs +++ b/demos/CommandLine/Utils/Config.cs @@ -17,7 +17,7 @@ public Config() DotEnv.Load(); Console.WriteLine($"Current directory: {Directory.GetCurrentDirectory()}"); - // Retrieve the environment variables + // Retrieve the environment variables (Not all of these are required) SupabaseUrl = Environment.GetEnvironmentVariable("SUPABASE_URL") ?? throw new InvalidOperationException("SUPABASE_URL environment variable is not set."); SupabaseAnonKey = Environment.GetEnvironmentVariable("SUPABASE_ANON_KEY") From 96bd1d7d206c73ac1bf8354ad48d50c8ce7a63b4 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Tue, 8 Apr 2025 11:26:51 +0200 Subject: [PATCH 5/8] Update .env.template and README to set USE_SUPABASE to false --- demos/CommandLine/.env.template | 2 +- demos/CommandLine/README.md | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/demos/CommandLine/.env.template b/demos/CommandLine/.env.template index 0747ad8..39d014f 100644 --- a/demos/CommandLine/.env.template +++ b/demos/CommandLine/.env.template @@ -6,4 +6,4 @@ SUPABASE_USERNAME=your-supabase-username SUPABASE_PASSWORD=your-supabase-password # Set to true if you want to use Supabase as the backend # Set to false if you want to use the Powersync backend -USE_SUPABASE=true \ No newline at end of file +USE_SUPABASE=false \ No newline at end of file diff --git a/demos/CommandLine/README.md b/demos/CommandLine/README.md index c2da674..354126b 100644 --- a/demos/CommandLine/README.md +++ b/demos/CommandLine/README.md @@ -11,7 +11,7 @@ This essentially uses anonymous authentication. A random user ID is generated an ## Connection Options -By default, this demo uses the NodeConnector for connecting to the PowerSync server. However, you can swap this out with the SupabaseConnector if needed: +By default, this demo uses the NodeConnector for connecting to the PowerSync server. However, you can swap this out with the SupabaseConnector if needed 1. Copy the `.env.template` file to a new `.env` file: ```bash @@ -32,11 +32,9 @@ By default, this demo uses the NodeConnector for connecting to the PowerSync ser SUPABASE_PASSWORD=your-supabase-password # Set to true if you want to use Supabase as the backend # Set to false if you want to use the Powersync backend - USE_SUPABASE=true + USE_SUPABASE=false ``` -3. Update your connector configuration to use SupabaseConnector instead of NodeConnector - ## Getting Started In the repo root, run the following to download the PowerSync extension: From d3c0e494451b9d9e3d671276ca447f9d4a9024a5 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Wed, 9 Apr 2025 13:55:17 +0200 Subject: [PATCH 6/8] Refactor Supabase integration: update .env.template, refactor models, and enhance configuration handling --- demos/CommandLine/.env.template | 32 +++++++++++---- demos/CommandLine/Demo.cs | 8 +--- .../CommandLine/Models/{ => Supabase}/List.cs | 4 +- .../Models/{ => Supabase}/Todos.cs | 4 +- demos/CommandLine/README.md | 4 ++ demos/CommandLine/SupabaseConnector.cs | 2 +- demos/CommandLine/Utils/Config.cs | 39 ++++++++++++------- 7 files changed, 60 insertions(+), 33 deletions(-) rename demos/CommandLine/Models/{ => Supabase}/List.cs (70%) rename demos/CommandLine/Models/{ => Supabase}/Todos.cs (81%) diff --git a/demos/CommandLine/.env.template b/demos/CommandLine/.env.template index 39d014f..bfbfab4 100644 --- a/demos/CommandLine/.env.template +++ b/demos/CommandLine/.env.template @@ -1,9 +1,25 @@ -SUPABASE_URL=your-supabase-url -SUPABASE_ANON_KEY=your_anon_key_here -POWERSYNC_URL=your-powersync-url -BACKEND_URL=your-backend-url -SUPABASE_USERNAME=your-supabase-username -SUPABASE_PASSWORD=your-supabase-password +# PowerSync server URL +POWERSYNC_URL=http://localhost:8080 + # Set to true if you want to use Supabase as the backend -# Set to false if you want to use the Powersync backend -USE_SUPABASE=false \ No newline at end of file +# Set to false if you want to use the PowerSync self-hosted backend +USE_SUPABASE=false + +# --- Supabase Connector Settings --- +# These values are used only if USE_SUPABASE=true + +# Supabase project URL +SUPABASE_URL=http://localhost:54321 + +# Supabase anon key (public client access) +SUPABASE_ANON_KEY=your_anon_key_here + +# Supabase credentials for an already existing user (used for login) +SUPABASE_USERNAME=your_supabase_email@example.com +SUPABASE_PASSWORD=your_supabase_password + +# --- PowerSync Backend Settings --- +# These values are used only if USE_SUPABASE=false + +# URL of your PowerSync self-hosted backend +BACKEND_URL=http://localhost:6060 diff --git a/demos/CommandLine/Demo.cs b/demos/CommandLine/Demo.cs index bca0867..347edd3 100644 --- a/demos/CommandLine/Demo.cs +++ b/demos/CommandLine/Demo.cs @@ -27,6 +27,7 @@ static async Task Main() { var supabaseConnector = new SupabaseConnector(config); + // Ensure this user already exists await supabaseConnector.Login(config.SupabaseUsername, config.SupabasePassword); connectorUserId = supabaseConnector.UserId; @@ -71,12 +72,6 @@ static async Task Main() } }); - // await db.Execute("insert into lists (id, name, owner_id, created_at) values (uuid(), 'New User33', ?, datetime())", [connector.UserId]); - - await db.Execute( - "UPDATE lists SET name = ?, created_at = datetime() WHERE owner_id = ? and id = ?", ["update CHCHCHCHCH", connectorUserId, "0bf55412-d35b-4814-ade9-daea4865df96"] - ); - var _ = Task.Run(async () => { while (running) @@ -118,7 +113,6 @@ await db.Execute( } }); - // Start live updating table await AnsiConsole.Live(panel) .StartAsync(async ctx => diff --git a/demos/CommandLine/Models/List.cs b/demos/CommandLine/Models/Supabase/List.cs similarity index 70% rename from demos/CommandLine/Models/List.cs rename to demos/CommandLine/Models/Supabase/List.cs index 4164c58..7839231 100644 --- a/demos/CommandLine/Models/List.cs +++ b/demos/CommandLine/Models/Supabase/List.cs @@ -1,9 +1,11 @@ -namespace CommandLine.Models; using Newtonsoft.Json; using Supabase.Postgrest.Attributes; using Supabase.Postgrest.Models; +namespace CommandLine.Models.Supabase; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. [Table("lists")] class List : BaseModel { diff --git a/demos/CommandLine/Models/Todos.cs b/demos/CommandLine/Models/Supabase/Todos.cs similarity index 81% rename from demos/CommandLine/Models/Todos.cs rename to demos/CommandLine/Models/Supabase/Todos.cs index fd7eb19..4ebea34 100644 --- a/demos/CommandLine/Models/Todos.cs +++ b/demos/CommandLine/Models/Supabase/Todos.cs @@ -1,9 +1,11 @@ -namespace CommandLine.Models; using Newtonsoft.Json; using Supabase.Postgrest.Attributes; using Supabase.Postgrest.Models; +namespace CommandLine.Models.Supabase; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. [Table("todos")] class Todo : BaseModel { diff --git a/demos/CommandLine/README.md b/demos/CommandLine/README.md index 354126b..662834d 100644 --- a/demos/CommandLine/README.md +++ b/demos/CommandLine/README.md @@ -9,6 +9,10 @@ Changes made to the backend's source DB or to the self-hosted web UI will be syn This essentially uses anonymous authentication. A random user ID is generated and stored in local storage. The backend returns a valid token which is not linked to a specific user. All data is synced to all users. +> **Note for Supabase users:** +> If you are using `USE_SUPABASE=true`, this demo expects a valid, **already existing Supabase user**. +> You must provide their credentials via the `.env` file using `SUPABASE_USERNAME` and `SUPABASE_PASSWORD`. + ## Connection Options By default, this demo uses the NodeConnector for connecting to the PowerSync server. However, you can swap this out with the SupabaseConnector if needed diff --git a/demos/CommandLine/SupabaseConnector.cs b/demos/CommandLine/SupabaseConnector.cs index ce02d0b..1e9fa0e 100644 --- a/demos/CommandLine/SupabaseConnector.cs +++ b/demos/CommandLine/SupabaseConnector.cs @@ -1,7 +1,7 @@ namespace CommandLine; using CommandLine.Helpers; -using CommandLine.Models; +using CommandLine.Models.Supabase; using CommandLine.Utils; using Newtonsoft.Json; using PowerSync.Common.Client; diff --git a/demos/CommandLine/Utils/Config.cs b/demos/CommandLine/Utils/Config.cs index 2ab3713..41f534e 100644 --- a/demos/CommandLine/Utils/Config.cs +++ b/demos/CommandLine/Utils/Config.cs @@ -2,6 +2,7 @@ namespace CommandLine.Utils; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. public class Config { public string SupabaseUrl { get; set; } @@ -17,26 +18,34 @@ public Config() DotEnv.Load(); Console.WriteLine($"Current directory: {Directory.GetCurrentDirectory()}"); - // Retrieve the environment variables (Not all of these are required) - SupabaseUrl = Environment.GetEnvironmentVariable("SUPABASE_URL") - ?? throw new InvalidOperationException("SUPABASE_URL environment variable is not set."); - SupabaseAnonKey = Environment.GetEnvironmentVariable("SUPABASE_ANON_KEY") - ?? throw new InvalidOperationException("SUPABASE_ANON_KEY environment variable is not set."); - PowerSyncUrl = Environment.GetEnvironmentVariable("POWERSYNC_URL") - ?? throw new InvalidOperationException("POWERSYNC_URL environment variable is not set."); - BackendUrl = Environment.GetEnvironmentVariable("BACKEND_URL") - ?? throw new InvalidOperationException("BACKEND_URL environment variable is not set."); - SupabaseUsername = Environment.GetEnvironmentVariable("SUPABASE_USERNAME") - ?? throw new InvalidOperationException("SUPABASE_USERNAME environment variable is not set."); - SupabasePassword = Environment.GetEnvironmentVariable("SUPABASE_PASSWORD") - ?? throw new InvalidOperationException("SUPABASE_PASSWORD environment variable is not set."); - - // Parse boolean value + // Parse boolean value first string useSupabaseStr = Environment.GetEnvironmentVariable("USE_SUPABASE") ?? "false"; if (!bool.TryParse(useSupabaseStr, out bool useSupabase)) { throw new InvalidOperationException("USE_SUPABASE environment variable is not a valid boolean."); } UseSupabase = useSupabase; + + Console.WriteLine("Use Supabase: " + UseSupabase); + + PowerSyncUrl = GetRequiredEnv("POWERSYNC_URL"); + + if (UseSupabase) + { + SupabaseUrl = GetRequiredEnv("SUPABASE_URL"); + SupabaseAnonKey = GetRequiredEnv("SUPABASE_ANON_KEY"); + SupabaseUsername = GetRequiredEnv("SUPABASE_USERNAME"); + SupabasePassword = GetRequiredEnv("SUPABASE_PASSWORD"); + } + else + { + BackendUrl = GetRequiredEnv("BACKEND_URL"); + } + } + + private static string GetRequiredEnv(string key) + { + return Environment.GetEnvironmentVariable(key) + ?? throw new InvalidOperationException($"{key} environment variable is not set."); } } \ No newline at end of file From b690e56fbd1dd85791642aa0a14bf9a4c708e51b Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Thu, 10 Apr 2025 10:56:37 +0200 Subject: [PATCH 7/8] Remove user_id.txt and add Microsoft.VisualBasic namespace to Todos model --- demos/CommandLine/Models/Supabase/Todos.cs | 1 + user_id.txt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 user_id.txt diff --git a/demos/CommandLine/Models/Supabase/Todos.cs b/demos/CommandLine/Models/Supabase/Todos.cs index 4ebea34..a871c86 100644 --- a/demos/CommandLine/Models/Supabase/Todos.cs +++ b/demos/CommandLine/Models/Supabase/Todos.cs @@ -1,4 +1,5 @@ +using Microsoft.VisualBasic; using Newtonsoft.Json; using Supabase.Postgrest.Attributes; using Supabase.Postgrest.Models; diff --git a/user_id.txt b/user_id.txt deleted file mode 100644 index 2d4a165..0000000 --- a/user_id.txt +++ /dev/null @@ -1 +0,0 @@ -72394969-fa2f-476b-822b-b2d5ba89acca \ No newline at end of file From ad1b4553a5927c1e3dfd9307a1290a7ba3aa2608 Mon Sep 17 00:00:00 2001 From: dean-journeyapps Date: Thu, 10 Apr 2025 11:02:21 +0200 Subject: [PATCH 8/8] Add user_id.txt to .gitignore to prevent tracking of sensitive information --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 05654a3..9bb7c5e 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ TestResults/ *.so .env + +# Ignore user id file +user_id.txt