Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ TestResults/
*.dylib
*.dll
*.so

.env
25 changes: 25 additions & 0 deletions demos/CommandLine/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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 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)
[email protected]
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
2 changes: 2 additions & 0 deletions demos/CommandLine/CommandLine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dotenv.Net" Version="3.2.1" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.1" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="supabase" Version="1.1.1" />

</ItemGroup>

Expand Down
24 changes: 24 additions & 0 deletions demos/CommandLine/CommandLine.sln
Original file line number Diff line number Diff line change
@@ -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
32 changes: 28 additions & 4 deletions demos/CommandLine/Demo.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
namespace CommandLine;

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()
{
Expand All @@ -16,7 +17,31 @@ static async Task Main()
});
await db.Init();

var connector = new NodeConnector();
var config = new Config();

IPowerSyncBackendConnector connector;

string connectorUserId = "";

if (config.UseSupabase)
{
var supabaseConnector = new SupabaseConnector(config);

// Ensure this user already exists
await supabaseConnector.Login(config.SupabaseUsername, config.SupabasePassword);

connectorUserId = supabaseConnector.UserId;

connector = supabaseConnector;
}
else
{
var nodeConnector = new NodeConnector(config);

connectorUserId = nodeConnector.UserId;

connector = nodeConnector;
}

var table = new Table()
.AddColumn("id")
Expand Down Expand Up @@ -60,7 +85,7 @@ static async Task Main()
}
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)
{
Expand Down Expand Up @@ -88,7 +113,6 @@ static async Task Main()
}
});


// Start live updating table
await AnsiConsole.Live(panel)
.StartAsync(async ctx =>
Expand Down
38 changes: 38 additions & 0 deletions demos/CommandLine/Helpers/SupabasePatchHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace CommandLine.Helpers;

using System.Linq.Expressions;
using Newtonsoft.Json;
using Supabase.Postgrest.Interfaces;
using Supabase.Postgrest.Models;

public static class SupabasePatchHelper
{
// Applies a "SET" operation to the table, setting the value of a specific property.
public static IPostgrestTable<T> ApplySet<T>(
IPostgrestTable<T> 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 on the model that matches the JSON property name
var property = typeof(T)
.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); // Check if the JSON property name matches

if (property == null)
throw new ArgumentException($"'{jsonPropertyName}' is not a valid property on type '{typeof(T).Name}'");

// 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<Func<T, object>>(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);
}
}
27 changes: 27 additions & 0 deletions demos/CommandLine/Models/Supabase/List.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

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
{
[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; }
}
43 changes: 43 additions & 0 deletions demos/CommandLine/Models/Supabase/Todos.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

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
{
[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; }
}
8 changes: 4 additions & 4 deletions demos/CommandLine/NodeConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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;
}
Expand Down
34 changes: 32 additions & 2 deletions demos/CommandLine/README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -9,6 +9,36 @@ 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

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_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=false
```

## Getting Started

In the repo root, run the following to download the PowerSync extension:
Expand All @@ -29,4 +59,4 @@ To run the Command-Line interface:

```bash
dotnet run Demo
```
```
Loading