Skip to content

Commit 8b3151a

Browse files
Add Supabase Connector integration and configuration support
1 parent 7576512 commit 8b3151a

12 files changed

+358
-3
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,5 @@ TestResults/
6161
*.dylib
6262
*.dll
6363
*.so
64+
65+
.env

demos/CommandLine/.env.template

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SUPABASE_URL=your-supabase-url
2+
SUPABASE_ANON_KEY=your_anon_key_here
3+
POWERSYNC_URL=your-powersync-url

demos/CommandLine/CommandLine.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
</PropertyGroup>
1414

1515
<ItemGroup>
16+
<PackageReference Include="Dotenv.Net" Version="3.2.1" />
1617
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.1" />
1718
<PackageReference Include="Spectre.Console" Version="0.49.1" />
19+
<PackageReference Include="supabase" Version="1.1.1" />
1820

1921
</ItemGroup>
2022

demos/CommandLine/CommandLine.sln

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio Version 17
3+
VisualStudioVersion = 17.5.2.0
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine", "CommandLine.csproj", "{6BB9F16E-3825-DE76-1286-9E5E2406710D}"
6+
EndProject
7+
Global
8+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9+
Debug|Any CPU = Debug|Any CPU
10+
Release|Any CPU = Release|Any CPU
11+
EndGlobalSection
12+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
13+
{6BB9F16E-3825-DE76-1286-9E5E2406710D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14+
{6BB9F16E-3825-DE76-1286-9E5E2406710D}.Debug|Any CPU.Build.0 = Debug|Any CPU
15+
{6BB9F16E-3825-DE76-1286-9E5E2406710D}.Release|Any CPU.ActiveCfg = Release|Any CPU
16+
{6BB9F16E-3825-DE76-1286-9E5E2406710D}.Release|Any CPU.Build.0 = Release|Any CPU
17+
EndGlobalSection
18+
GlobalSection(SolutionProperties) = preSolution
19+
HideSolutionNode = FALSE
20+
EndGlobalSection
21+
GlobalSection(ExtensibilityGlobals) = postSolution
22+
SolutionGuid = {A5588511-5909-4F05-80EB-09A56805607C}
23+
EndGlobalSection
24+
EndGlobal

demos/CommandLine/Demo.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace CommandLine;
22

3+
using CommandLine.Utils;
34
using PowerSync.Common.Client;
45
using Spectre.Console;
56

@@ -16,7 +17,11 @@ static async Task Main()
1617
});
1718
await db.Init();
1819

19-
var connector = new NodeConnector();
20+
//var connector = new NodeConnector();
21+
var config = new SupabaseConfig();
22+
var connector = new SupabaseConnector(config);
23+
24+
await connector.Login("[email protected]", "Dean1998");
2025

2126
var table = new Table()
2227
.AddColumn("id")
@@ -47,6 +52,12 @@ static async Task Main()
4752
}
4853
});
4954

55+
// await db.Execute("insert into lists (id, name, owner_id, created_at) values (uuid(), 'New User33', ?, datetime())", [connector.UserId]);
56+
57+
await db.Execute(
58+
"UPDATE lists SET name = ?, created_at = datetime() WHERE owner_id = ? and id = ?", ["update CHCHCHCHCH" , connector.UserId, "0bf55412-d35b-4814-ade9-daea4865df96"]
59+
);
60+
5061
var _ = Task.Run(async () =>
5162
{
5263
while (running)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace CommandLine.Helpers;
2+
3+
using System.Linq.Expressions;
4+
using Newtonsoft.Json;
5+
using Supabase.Postgrest.Interfaces;
6+
using Supabase.Postgrest.Models;
7+
8+
public static class SupabasePatchHelper
9+
{
10+
public static IPostgrestTable<T> ApplySet<T>(
11+
IPostgrestTable<T> table,
12+
string jsonPropertyName,
13+
object value
14+
) where T : BaseModel, new()
15+
{
16+
// Find the property that matches the JsonProperty name
17+
var property = typeof(T)
18+
.GetProperties()
19+
.FirstOrDefault(p =>
20+
p.GetCustomAttributes(typeof(JsonPropertyAttribute), true)
21+
.FirstOrDefault() is JsonPropertyAttribute attr &&
22+
attr.PropertyName == jsonPropertyName);
23+
24+
if (property == null)
25+
throw new ArgumentException($"'{jsonPropertyName}' is not a valid property on type '{typeof(T).Name}'");
26+
27+
var parameter = Expression.Parameter(typeof(T), "x");
28+
var propertyAccess = Expression.Property(parameter, property.Name);
29+
var converted = Expression.Convert(propertyAccess, typeof(object));
30+
var lambda = Expression.Lambda<Func<T, object>>(converted, parameter);
31+
32+
return table.Set(lambda, value);
33+
}
34+
}

demos/CommandLine/Models/List.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace CommandLine.Models;
2+
3+
using Newtonsoft.Json;
4+
using Supabase.Postgrest.Attributes;
5+
using Supabase.Postgrest.Models;
6+
7+
[Table("lists")]
8+
class List : BaseModel
9+
{
10+
[PrimaryKey("id")]
11+
[JsonProperty("id")]
12+
public string Id { get; set; }
13+
14+
[Column("created_at")]
15+
[JsonProperty("created_at")]
16+
public string CreatedAt { get; set; }
17+
18+
[Column("name")]
19+
[JsonProperty("name")]
20+
public string Name { get; set; }
21+
22+
[Column("owner_id")]
23+
[JsonProperty("owner_id")]
24+
public string OwnerId { get; set; }
25+
}

demos/CommandLine/Models/Todos.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace CommandLine.Models;
2+
3+
using Newtonsoft.Json;
4+
using Supabase.Postgrest.Attributes;
5+
using Supabase.Postgrest.Models;
6+
7+
[Table("todos")]
8+
class Todo : BaseModel
9+
{
10+
[PrimaryKey("id")]
11+
[JsonProperty("id")]
12+
public string Id { get; set; }
13+
14+
[Column("list_id")]
15+
[JsonProperty("list_id")]
16+
public string ListId { get; set; }
17+
18+
[Column("created_at")]
19+
[JsonProperty("created_at")]
20+
public string CreatedAt { get; set; }
21+
22+
[Column("completed_at")]
23+
[JsonProperty("completed_at")]
24+
public string CompletedAt { get; set; }
25+
26+
[Column("description")]
27+
[JsonProperty("description")]
28+
public string Description { get; set; }
29+
30+
[Column("created_by")]
31+
[JsonProperty("created_by")]
32+
public string CreatedBy { get; set; }
33+
34+
[Column("completed_by")]
35+
[JsonProperty("completed_by")]
36+
public string CompletedBy { get; set; }
37+
38+
[Column("completed")]
39+
[JsonProperty("completed")]
40+
public int Completed { get; set; }
41+
}

demos/CommandLine/README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# PowerSync CLI demo app
1+
# PowerSync CLI Demo App
22

33
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.
44
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
99

1010
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.
1111

12+
## Connection Options
13+
14+
By default, this demo uses the NodeConnector for connecting to the PowerSync server. However, you can swap this out with the SupabaseConnector if needed:
15+
16+
1. Copy the `.env.template` file to a new `.env` file:
17+
```bash
18+
# On Linux/macOS
19+
cp .env.template .env
20+
21+
# On Windows
22+
copy .env.template .env
23+
```
24+
25+
2. Replace the necessary fields in the `.env` file with your Supabase and PowerSync credentials:
26+
```
27+
SUPABASE_URL=your_supabase_url
28+
SUPABASE_ANON_KEY=your_supabase_anon_key
29+
POWERSYNC_URL=your_powersync_url
30+
```
31+
32+
3. Update your connector configuration to use SupabaseConnector instead of NodeConnector
33+
1234
## Getting Started
1335

1436
In the repo root, run the following to download the PowerSync extension:
@@ -29,4 +51,4 @@ To run the Command-Line interface:
2951

3052
```bash
3153
dotnet run Demo
32-
```
54+
```
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
namespace CommandLine;
2+
3+
using CommandLine.Helpers;
4+
using CommandLine.Models;
5+
using CommandLine.Utils;
6+
using Newtonsoft.Json;
7+
using PowerSync.Common.Client;
8+
using PowerSync.Common.Client.Connection;
9+
using PowerSync.Common.DB.Crud;
10+
using Supabase;
11+
using Supabase.Gotrue;
12+
using Supabase.Postgrest.Exceptions;
13+
using Supabase.Postgrest.Interfaces;
14+
15+
public class SupabaseConnector : IPowerSyncBackendConnector
16+
{
17+
private readonly Supabase.Client _supabase;
18+
private readonly SupabaseConfig _config;
19+
private Session? _currentSession;
20+
21+
public Session? CurrentSession
22+
{
23+
get => _currentSession;
24+
set
25+
{
26+
_currentSession = value;
27+
28+
if (_currentSession?.User?.Id != null)
29+
{
30+
UserId = _currentSession.User.Id;
31+
}
32+
}
33+
}
34+
35+
public string UserId { get; private set; } = "";
36+
37+
public bool Ready { get; private set; }
38+
39+
public SupabaseConnector(SupabaseConfig config)
40+
{
41+
_config = config;
42+
_supabase = new Supabase.Client(config.SupabaseUrl, config.SupabaseAnonKey, new SupabaseOptions
43+
{
44+
AutoConnectRealtime = true
45+
});
46+
47+
_ = _supabase.InitializeAsync();
48+
}
49+
50+
public async Task Login(string email, string password)
51+
{
52+
var response = await _supabase.Auth.SignInWithPassword(email, password);
53+
if (response?.User == null || response.AccessToken == null)
54+
{
55+
throw new Exception("Login failed.");
56+
}
57+
58+
CurrentSession = response;
59+
}
60+
61+
public Task<PowerSyncCredentials?> FetchCredentials()
62+
{
63+
PowerSyncCredentials? credentials = null;
64+
65+
var sessionResponse = _supabase.Auth.CurrentSession;
66+
if (sessionResponse?.AccessToken != null)
67+
{
68+
credentials = new PowerSyncCredentials(_config.PowerSyncUrl, sessionResponse.AccessToken);
69+
}
70+
71+
return Task.FromResult(credentials);
72+
}
73+
74+
public async Task UploadData(IPowerSyncDatabase database)
75+
{
76+
var transaction = await database.GetNextCrudTransaction();
77+
if (transaction == null) return;
78+
79+
try
80+
{
81+
foreach (var op in transaction.Crud)
82+
{
83+
switch (op.Op)
84+
{
85+
case UpdateType.PUT:
86+
if (op.Table.ToLower().Trim() == "lists")
87+
{
88+
var model = JsonConvert.DeserializeObject<List>(JsonConvert.SerializeObject(op.OpData)) ?? throw new InvalidOperationException("Model is null.");
89+
model.Id = op.Id;
90+
91+
await _supabase.From<List>().Upsert(model);
92+
}
93+
else if (op.Table.ToLower().Trim() == "todos")
94+
{
95+
var model = JsonConvert.DeserializeObject<Todo>(JsonConvert.SerializeObject(op.OpData)) ?? throw new InvalidOperationException("Model is null.");
96+
model.Id = op.Id;
97+
98+
await _supabase.From<Todo>().Upsert(model);
99+
}
100+
break;
101+
102+
case UpdateType.PATCH:
103+
if (op.OpData is null || op.OpData.Count == 0)
104+
{
105+
Console.WriteLine("PATCH skipped: No data to update.");
106+
break;
107+
}
108+
109+
if (op.Table.ToLower().Trim() == "lists")
110+
{
111+
IPostgrestTable<List> updateQuery = _supabase
112+
.From<List>()
113+
.Where(x => x.Id == op.Id);
114+
115+
foreach (var kvp in op.OpData)
116+
{
117+
updateQuery = SupabasePatchHelper.ApplySet(updateQuery, kvp.Key, kvp.Value);
118+
}
119+
120+
_ = await updateQuery.Update();
121+
}
122+
else if (op.Table.ToLower().Trim() == "todos")
123+
{
124+
IPostgrestTable<Todo> updateQuery = _supabase
125+
.From<Todo>()
126+
.Where(x => x.Id == op.Id);
127+
128+
foreach (var kvp in op.OpData)
129+
{
130+
updateQuery = SupabasePatchHelper.ApplySet(updateQuery, kvp.Key, kvp.Value);
131+
}
132+
133+
_ = await updateQuery.Update();
134+
}
135+
break;
136+
137+
case UpdateType.DELETE:
138+
if (op.Table.ToLower().Trim() == "lists")
139+
{
140+
await _supabase
141+
.From<List>()
142+
.Where(x => x.Id == op.Id)
143+
.Delete();
144+
}
145+
else if (op.Table.ToLower().Trim() == "todos")
146+
{
147+
await _supabase
148+
.From<Todo>()
149+
.Where(x => x.Id == op.Id)
150+
.Delete();
151+
}
152+
break;
153+
154+
default:
155+
throw new InvalidOperationException("Unknown operation type.");
156+
}
157+
}
158+
159+
await transaction.Complete();
160+
}
161+
catch (PostgrestException ex)
162+
{
163+
Console.WriteLine($"Error during upload: {ex.Message}");
164+
throw;
165+
}
166+
}
167+
}

0 commit comments

Comments
 (0)