Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: enhancement of postgres change handlers #54

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Changes from all 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
15 changes: 7 additions & 8 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -23,13 +23,12 @@ jobs:
- name: Build
run: dotnet build --configuration Release --no-restore

#- name: Add hosts entries
# run: |
# echo "127.0.0.1 realtime-dev.localhost" | sudo tee -a /etc/hosts
# echo "172.17.0.1 host.docker.internal" | sudo tee -a /etc/hosts
- uses: supabase/setup-cli@v1
with:
version: latest

#- name: Initialize Testing Stack
# run: docker-compose up -d
- name: Start Supabsae
run: supabase start

#- name: Test
# run: dotnet test --no-restore
- name: Test
run: dotnet test --no-restore
6 changes: 3 additions & 3 deletions Realtime/Client.cs
Original file line number Diff line number Diff line change
@@ -47,15 +47,15 @@
public ClientOptions Options { get; }

private Func<Dictionary<string, string>>? _getHeaders { get; set; }

/// <inheritdoc />
public Func<Dictionary<string, string>>? GetHeaders
{
get => _getHeaders;
set
{
_getHeaders = value;

if (Socket != null)
Socket.GetHeaders = value;
}
@@ -345,7 +345,7 @@
/// <exception cref="Exception"></exception>
public RealtimeChannel Channel(string channelName)
{
var topic = $"realtime:{channelName}";
var topic = channelName.StartsWith("realtime:") ? channelName : $"realtime:{channelName}";

if (_subscriptions.TryGetValue(topic, out var channel))
return channel;
@@ -386,7 +386,7 @@
var options = new ChannelOptions(Options, () => AccessToken, SerializerSettings);

var subscription = new RealtimeChannel(Socket!, key, options);
subscription.Register(changesOptions);

Check warning on line 389 in Realtime/Client.cs

GitHub Actions / build-and-test

'RealtimeChannel.Register(PostgresChangesOptions)' is obsolete: 'Use OnPostgresChange instead.'

Check warning on line 389 in Realtime/Client.cs

GitHub Actions / build-and-test

'RealtimeChannel.Register(PostgresChangesOptions)' is obsolete: 'Use OnPostgresChange instead.'

Check warning on line 389 in Realtime/Client.cs

GitHub Actions / build-and-test

'RealtimeChannel.Register(PostgresChangesOptions)' is obsolete: 'Use OnPostgresChange instead.'

Check warning on line 389 in Realtime/Client.cs

GitHub Actions / build-and-test

'RealtimeChannel.Register(PostgresChangesOptions)' is obsolete: 'Use OnPostgresChange instead.'

_subscriptions.Add(key, subscription);

11 changes: 11 additions & 0 deletions Realtime/Interfaces/IRealtimeChannel.cs
Original file line number Diff line number Diff line change
@@ -97,6 +97,17 @@ public interface IRealtimeChannel
/// </summary>
string Topic { get; }

/// <summary>
/// Registers and adds a postgres change handler.
/// </summary>
/// <param name="postgresChangeHandler">The handler to process the event.</param>
/// <param name="listenType">The type of event this callback should process.</param>
/// <param name="schema">The schema to listen to.</param>
/// <param name="table">The table to listen to.</param>
/// <param name="filter">The filter to apply.</param>
/// <returns></returns>
public IRealtimeChannel OnPostgresChange(PostgresChangesHandler postgresChangeHandler, ListenType listenType = ListenType.All, string schema = "public", string? table = null, string? filter = null);

/// <summary>
/// Add a state changed listener
/// </summary>
3 changes: 2 additions & 1 deletion Realtime/Interfaces/IRealtimeClient.cs
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ namespace Supabase.Realtime.Interfaces;
/// </summary>
/// <typeparam name="TSocket"></typeparam>
/// <typeparam name="TChannel"></typeparam>
public interface IRealtimeClient<TSocket, TChannel>: IGettableHeaders
public interface IRealtimeClient<TSocket, TChannel> : IGettableHeaders
where TSocket : IRealtimeSocket
where TChannel : IRealtimeChannel
{
@@ -95,6 +95,7 @@ public interface IRealtimeClient<TSocket, TChannel>: IGettableHeaders
/// <param name="value"></param>
/// <param name="parameters"></param>
/// <returns></returns>
[Obsolete("Please use Channel(string channelName) instead.")]
TChannel Channel(string database = "realtime", string schema = "public", string table = "*",
string? column = null, string? value = null, Dictionary<string, string>? parameters = null);

3 changes: 2 additions & 1 deletion Realtime/PostgresChanges/PostgresChangesOptions.cs
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@
/// <summary>
/// The table for this listener, can be: `*` matching all tables in schema.
/// </summary>
[JsonProperty("table")]
[JsonProperty("table", NullValueHandling = NullValueHandling.Ignore)]
public string? Table { get; set; }

/// <summary>
@@ -71,6 +71,7 @@
/// The parameters passed to the server
/// </summary>
[JsonProperty("parameters", NullValueHandling = NullValueHandling.Ignore)]
[System.Obsolete("The Parameters property is deprecated and will be removed in a future version.")]
public Dictionary<string, string>? Parameters { get; set; }

/// <summary>
@@ -95,5 +96,5 @@
Schema = schema;
Table = table;
Filter = filter;
Parameters = parameters;

Check warning on line 99 in Realtime/PostgresChanges/PostgresChangesOptions.cs

GitHub Actions / build-and-test

'PostgresChangesOptions.Parameters' is obsolete: 'The Parameters property is deprecated and will be removed in a future version.'

Check warning on line 99 in Realtime/PostgresChanges/PostgresChangesOptions.cs

GitHub Actions / build-and-test

'PostgresChangesOptions.Parameters' is obsolete: 'The Parameters property is deprecated and will be removed in a future version.'

Check warning on line 99 in Realtime/PostgresChanges/PostgresChangesOptions.cs

GitHub Actions / build-and-test

'PostgresChangesOptions.Parameters' is obsolete: 'The Parameters property is deprecated and will be removed in a future version.'

Check warning on line 99 in Realtime/PostgresChanges/PostgresChangesOptions.cs

GitHub Actions / build-and-test

'PostgresChangesOptions.Parameters' is obsolete: 'The Parameters property is deprecated and will be removed in a future version.'
}
25 changes: 22 additions & 3 deletions Realtime/RealtimeChannel.cs
Original file line number Diff line number Diff line change
@@ -324,10 +324,28 @@
}

/// <summary>
/// Add a postgres changes listener. Should be paired with <see cref="Register"/>.
/// Registers and adds a postgres change handler.
/// </summary>
/// <param name="postgresChangeHandler">The handler to process the event.</param>
/// <param name="listenType">The type of event this callback should process.</param>
/// <param name="schema">The schema to listen to.</param>
/// <param name="table">The table to listen to.</param>
/// <param name="filter">The filter to apply.</param>
/// <returns></returns>
public IRealtimeChannel OnPostgresChange(PostgresChangesHandler postgresChangeHandler, ListenType listenType = ListenType.All, string schema = "public", string? table = null, string? filter = null)
{
var postgresChangesOptions = new PostgresChangesOptions(schema, table, listenType, filter);
Register(postgresChangesOptions);

Check warning on line 338 in Realtime/RealtimeChannel.cs

GitHub Actions / build-and-test

'RealtimeChannel.Register(PostgresChangesOptions)' is obsolete: 'Use OnPostgresChange instead.'

Check warning on line 338 in Realtime/RealtimeChannel.cs

GitHub Actions / build-and-test

'RealtimeChannel.Register(PostgresChangesOptions)' is obsolete: 'Use OnPostgresChange instead.'

Check warning on line 338 in Realtime/RealtimeChannel.cs

GitHub Actions / build-and-test

'RealtimeChannel.Register(PostgresChangesOptions)' is obsolete: 'Use OnPostgresChange instead.'

Check warning on line 338 in Realtime/RealtimeChannel.cs

GitHub Actions / build-and-test

'RealtimeChannel.Register(PostgresChangesOptions)' is obsolete: 'Use OnPostgresChange instead.'
AddPostgresChangeHandler(listenType, postgresChangeHandler);

Check warning on line 339 in Realtime/RealtimeChannel.cs

GitHub Actions / build-and-test

'RealtimeChannel.AddPostgresChangeHandler(PostgresChangesOptions.ListenType, IRealtimeChannel.PostgresChangesHandler)' is obsolete: 'Use OnPostgresChange instead.'

Check warning on line 339 in Realtime/RealtimeChannel.cs

GitHub Actions / build-and-test

'RealtimeChannel.AddPostgresChangeHandler(PostgresChangesOptions.ListenType, IRealtimeChannel.PostgresChangesHandler)' is obsolete: 'Use OnPostgresChange instead.'

Check warning on line 339 in Realtime/RealtimeChannel.cs

GitHub Actions / build-and-test

'RealtimeChannel.AddPostgresChangeHandler(PostgresChangesOptions.ListenType, IRealtimeChannel.PostgresChangesHandler)' is obsolete: 'Use OnPostgresChange instead.'
return this;
}

/// <summary>
/// Adds a postgres changes listener. Should be paired with <see cref="Register"/>.
/// </summary>
/// <param name="listenType">The type of event this callback should process.</param>
/// <param name="postgresChangeHandler"></param>
[Obsolete("Use OnPostgresChange instead.")]
public void AddPostgresChangeHandler(ListenType listenType, PostgresChangesHandler postgresChangeHandler)
{
if (!_postgresChangesHandlers.ContainsKey(listenType))
@@ -425,6 +443,7 @@
/// </summary>
/// <param name="postgresChangesOptions"></param>
/// <returns></returns>
[Obsolete("Use OnPostgresChange instead.")]
public IRealtimeChannel Register(PostgresChangesOptions postgresChangesOptions)
{
PostgresChangesOptions.Add(postgresChangesOptions);
@@ -694,7 +713,7 @@
_isRejoining = false;

NotifyErrorOccurred(new RealtimeException(message.Json)
{ Reason = FailureHint.Reason.ChannelJoinFailure });
{ Reason = FailureHint.Reason.ChannelJoinFailure });
break;
}
}
@@ -733,7 +752,7 @@
break;
case PhoenixStatusError:
NotifyErrorOccurred(new RealtimeException(message.Json)
{ Reason = FailureHint.Reason.ChannelJoinFailure });
{ Reason = FailureHint.Reason.ChannelJoinFailure });
break;
}

127 changes: 96 additions & 31 deletions RealtimeTests/ChannelPostgresChangesTests.cs
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@
using RealtimeTests.Models;
using Supabase.Realtime;
using Supabase.Realtime.Interfaces;
using Supabase.Realtime.PostgresChanges;
using static Supabase.Realtime.Constants;
using static Supabase.Realtime.PostgresChanges.PostgresChangesOptions;

@@ -38,15 +37,13 @@ public async Task ChannelPayloadReturnsModel()
{
var tsc = new TaskCompletionSource<bool>();

var channel = _socketClient!.Channel("example");
channel.Register(new PostgresChangesOptions("public", "*"));
channel.AddPostgresChangeHandler(ListenType.Inserts, (_, changes) =>
{
var model = changes.Model<Todo>();
tsc.SetResult(model != null);
});

await channel.Subscribe();
await _socketClient!.Channel("example")
.OnPostgresChange((_, changes) =>
{
var model = changes.Model<Todo>();
tsc.SetResult(model != null);
}, ListenType.Inserts)
.Subscribe();

await _restClient!.Table<Todo>().Insert(new Todo { UserId = 1, Details = "Client Models a response? ✅" });

@@ -59,11 +56,10 @@ public async Task ChannelReceivesInsertCallback()
{
var tsc = new TaskCompletionSource<bool>();

var channel = _socketClient!.Channel("realtime", "public", "todos");

channel.AddPostgresChangeHandler(ListenType.Inserts, (_, _) => tsc.SetResult(true));
await _socketClient!.Channel("realtime:public:todos")
.OnPostgresChange((_, _) => tsc.SetResult(true), ListenType.Inserts, table: "todos")
.Subscribe();

await channel.Subscribe();
await _restClient!.Table<Todo>()
.Insert(new Todo { UserId = 1, Details = "Client receives insert callback? ✅" });

@@ -83,9 +79,8 @@ public async Task ChannelReceivesUpdateCallback()
var oldDetails = model.Details;
var newDetails = $"I'm an updated item ✏️ - {DateTime.Now}";

var channel = _socketClient!.Channel("realtime", "public", "todos");

channel.AddPostgresChangeHandler(ListenType.Updates, (_, changes) =>
await _socketClient!.Channel("realtime:public:todos")
.OnPostgresChange((_, changes) =>
{
var oldModel = changes.OldModel<Todo>();

@@ -101,9 +96,8 @@ public async Task ChannelReceivesUpdateCallback()
}

tsc.SetResult(true);
});

await channel.Subscribe();
}, ListenType.Updates, table: "todos")
.Subscribe();

await _restClient.Table<Todo>()
.Set(x => x.Details!, newDetails)
@@ -119,11 +113,9 @@ public async Task ChannelReceivesDeleteCallback()
{
var tsc = new TaskCompletionSource<bool>();

var channel = _socketClient!.Channel("realtime", "public", "todos");

channel.AddPostgresChangeHandler(ListenType.Deletes, (_, _) => tsc.SetResult(true));

await channel.Subscribe();
await _socketClient!.Channel("realtime:public:todos")
.OnPostgresChange((_, _) => tsc.SetResult(true), ListenType.Deletes, table: "todos")
.Subscribe();

var result = await _restClient!.Table<Todo>().Get();
var model = result.Models.Last();
@@ -143,9 +135,7 @@ public async Task ChannelReceivesWildcardCallback()

List<Task> tasks = new List<Task> { insertTsc.Task, updateTsc.Task, deleteTsc.Task };

var channel = _socketClient!.Channel("realtime", "public", "todos");

channel.AddPostgresChangeHandler(ListenType.All, (_, changes) =>
await _socketClient!.Channel("realtime:public:todos").OnPostgresChange((_, changes) =>
{
switch (changes.Payload?.Data?.Type)
{
@@ -159,12 +149,11 @@ public async Task ChannelReceivesWildcardCallback()
deleteTsc.SetResult(true);
break;
}
});

await channel.Subscribe();
}, ListenType.All, table: "todos").Subscribe();

var modeledResponse = await _restClient!.Table<Todo>().Insert(new Todo
{ UserId = 1, Details = "Client receives wildcard callbacks? ✅" });
{ UserId = 1, Details = "Client receives wildcard callbacks? ✅" });
var newModel = modeledResponse.Models.First();

await _restClient.Table<Todo>().Set(x => x.Details!, "And edits.").Match(newModel).Update();
@@ -176,4 +165,80 @@ public async Task ChannelReceivesWildcardCallback()
Assert.IsTrue(updateTsc.Task.Result);
Assert.IsTrue(deleteTsc.Task.Result);
}

[TestMethod("Channel: Receives Multiple Handlers")]
public async Task ChannelReceivesMultipleHandlers()
{
var insertTsc = new TaskCompletionSource<bool>();
var updateTsc = new TaskCompletionSource<bool>();
var deleteTsc = new TaskCompletionSource<bool>();
var allHandlerTsc = new TaskCompletionSource<bool>();
var filterHandlerTsc = new TaskCompletionSource<bool>();

var insertHandlerCalledCount = 0;
var updateHandlerCalledCount = 0;
var deleteHandlerCalledCount = 0;
var allHandlerCalledCount = 0;
var filterHandlerCalledCount = 0;

var channel = _socketClient!.Channel("realtime:public:todos");

channel.OnPostgresChange((_, changes) =>
{
if (changes.Payload?.Data?.Type == EventType.Insert)
{
insertHandlerCalledCount += 1;
insertTsc.SetResult(true);
}
}, ListenType.Inserts, table: "todos");

channel.OnPostgresChange((_, changes) =>
{
if (changes.Payload?.Data?.Type == EventType.Update)
{
updateHandlerCalledCount += 1;
updateTsc.SetResult(true);
}
}, ListenType.Updates, table: "todos");

channel.OnPostgresChange((_, changes) =>
{
if (changes.Payload?.Data?.Type == EventType.Delete)
{
deleteHandlerCalledCount += 1;
deleteTsc.SetResult(true);
}
}, ListenType.Deletes, table: "todos");

channel.OnPostgresChange((_, _) =>
{
allHandlerCalledCount += 1;
allHandlerTsc.SetResult(true);
}, ListenType.All, table: "todos");

channel.OnPostgresChange((_, changes) =>
{
filterHandlerCalledCount += 1;
filterHandlerTsc.SetResult(true);
}, ListenType.Updates, table: "todos");

await channel.Subscribe();

var modeledResponse = await _restClient!.Table<Todo>().Insert(new Todo
{ UserId = 1, Details = "Testing multiple handlers" });
var newModel = modeledResponse.Models.First();

await _restClient.Table<Todo>().Set(x => x.Details!, "Filtered update").Match(newModel).Update();
await _restClient.Table<Todo>().Set(x => x.Details!, "Another update").Match(newModel).Update();
await _restClient.Table<Todo>().Match(newModel).Delete();

await Task.WhenAll(insertTsc.Task, updateTsc.Task, deleteTsc.Task, allHandlerTsc.Task, filterHandlerTsc.Task);

Assert.AreEqual(insertHandlerCalledCount, 1);
Assert.AreEqual(updateHandlerCalledCount, 2);
Assert.AreEqual(deleteHandlerCalledCount, 1);

Assert.AreEqual(allHandlerCalledCount, 4);
Assert.AreEqual(filterHandlerCalledCount, 1);
}
}
32 changes: 10 additions & 22 deletions RealtimeTests/ChannelTests.cs
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ public async Task ChannelCloseEventHandler()
{
var tsc = new TaskCompletionSource<bool>();

var channel = _socketClient!.Channel("realtime", "public", "todos");
var channel = _socketClient!.Channel("realtime:public:todos");
channel.AddStateChangedHandler((_, state) =>
{
if (state == ChannelState.Closed)
@@ -57,41 +57,29 @@ public async Task ChannelSupportsWalrusArray()
Todo? result = null;
var tsc = new TaskCompletionSource<bool>();

var channel = _socketClient!.Channel("realtime", "public", "todos");
var channel = _socketClient!.Channel("realtime:public:todos");
var numbers = new List<int> { 4, 5, 6 };

await channel.Subscribe();

channel.AddPostgresChangeHandler(ListenType.Inserts, (_, changes) =>
channel.OnPostgresChange((_, changes) =>
{
result = changes.Model<Todo>();
tsc.SetResult(true);
});
}, ListenType.Inserts);

await channel.Subscribe();

await _restClient!.Table<Todo>().Insert(new Todo { UserId = 1, Numbers = numbers });

await tsc.Task;
CollectionAssert.AreEqual(numbers, result?.Numbers);
}

[TestMethod("Channel: Sends Join parameters")]
public async Task ChannelSendsJoinParameters()
{
var parameters = new Dictionary<string, string> { { "key", "value" } };
var channel = _socketClient!.Channel("realtime", "public", "todos", parameters: parameters);

await channel.Subscribe();

var serialized = JsonConvert.SerializeObject(channel.JoinPush?.Payload);
Assert.IsTrue(serialized.Contains("\"key\":\"value\""));
}

[TestMethod("Channel: Returns single subscription per unique topic.")]
public async Task ChannelJoinsDuplicateSubscription()
{
var subscription1 = _socketClient!.Channel("realtime", "public", "todos");
var subscription2 = _socketClient!.Channel("realtime", "public", "todos");
var subscription3 = _socketClient!.Channel("realtime", "public", "todos", "user_id", "1");
var subscription1 = _socketClient!.Channel("realtime:public:todos");
var subscription2 = _socketClient!.Channel("realtime:public:todos");
var subscription3 = _socketClient!.Channel("realtime:public:todos:user_id:1");

Assert.AreEqual(subscription1.Topic, subscription2.Topic);

@@ -100,7 +88,7 @@ public async Task ChannelJoinsDuplicateSubscription()
Assert.AreEqual(subscription1.HasJoinedOnce, subscription2.HasJoinedOnce);
Assert.AreNotEqual(subscription1.HasJoinedOnce, subscription3.HasJoinedOnce);

var subscription4 = _socketClient!.Channel("realtime", "public", "todos");
var subscription4 = _socketClient!.Channel("realtime:public:todos");

Assert.AreEqual(subscription1.HasJoinedOnce, subscription4.HasJoinedOnce);
}
10 changes: 5 additions & 5 deletions RealtimeTests/ClientTests.cs
Original file line number Diff line number Diff line change
@@ -103,8 +103,8 @@ public async Task ClientCanRemoveChannelSubscription()
[TestMethod("Client: SetsAuth")]
public async Task ClientSetsAuth()
{
var channel = client!.Channel("realtime", "public", "todos");
var channel2 = client!.Channel("realtime", "public", "todos");
var channel = client!.Channel("realtime:public:todos");
var channel2 = client!.Channel("realtime:public:todos");

var token =
@"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.C8oVtF5DICct_4HcdSKt8pdrxBFMQOAnPpbiiUbaXAY";
@@ -137,13 +137,13 @@ public async Task ClientCanReconnectAfterProgrammaticDisconnect()
public async Task ClientCanSetHeaders()
{
client!.Disconnect();

client!.GetHeaders = () => new Dictionary<string, string>() { { "testing", "123" } };
await client.ConnectAsync();

Assert.IsNotNull(client!);
Assert.IsNotNull(client!.Socket);
Assert.IsNotNull(client!.Socket.GetHeaders);
Assert.AreEqual("123",client.Socket.GetHeaders()["testing"]);
Assert.AreEqual("123", client.Socket.GetHeaders()["testing"]);
}
}
7 changes: 3 additions & 4 deletions RealtimeTests/Helpers.cs
Original file line number Diff line number Diff line change
@@ -7,11 +7,10 @@ namespace RealtimeTests;

internal static class Helpers
{
private const string ApiKey =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIiLCJpYXQiOjE2NzEyMzc4NzMsImV4cCI6MjAwMjc3Mzk5MywiYXVkIjoiIiwic3ViIjoiIiwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifQ.qoYdljDZ9rjfs1DKj5_OqMweNtj7yk20LZKlGNLpUO8";
private const string ApiKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0";

private const string SocketEndpoint = "ws://realtime-dev.localhost:4000/socket";
private const string RestEndpoint = "http://localhost:3000";
private const string SocketEndpoint = "ws://127.0.0.1:54321/realtime/v1";
private const string RestEndpoint = "http://localhost:54321/rest/v1";

public static Supabase.Postgrest.Client RestClient() => new(RestEndpoint, new Supabase.Postgrest.ClientOptions());

4 changes: 4 additions & 0 deletions supabase/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Supabase
.branches
.temp
.env
278 changes: 278 additions & 0 deletions supabase/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
# For detailed configuration reference documentation, visit:
# https://supabase.com/docs/guides/local-development/cli/config
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "realtime-csharp"

[api]
enabled = true
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"]
# Extra schemas to add to the search_path of every request.
extra_search_path = ["public", "extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000

[api.tls]
# Enable HTTPS endpoints locally using a self-signed certificate.
enabled = false

[db]
# Port to use for the local database URL.
port = 54322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 15

[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
# How many server connections to allow per user/database pair.
default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100

[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed.sql"]

[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096

[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"

# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[inbucket]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326
# admin_email = "admin@email.com"
# sender_name = "Admin"

[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"

# Image transformation API is available to Supabase Pro plan.
# [storage.image_transformation]
# enabled = true

# Uncomment to configure local storage buckets
# [storage.buckets.images]
# public = false
# file_size_limit = "50MiB"
# allowed_mime_types = ["image/png", "image/jpeg"]
# objects_path = "./images"

[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
# Requires enable_refresh_token_rotation = true.
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow anonymous sign-ins to your project.
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
minimum_password_length = 6
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
password_requirements = ""

[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
# Number of characters used in the email OTP.
otp_length = 6
# Number of seconds before the email OTP expires (defaults to 1 hour).
otp_expiry = 3600

# Use a production-ready SMTP server
# [auth.email.smtp]
# enabled = true
# host = "smtp.sendgrid.net"
# port = 587
# user = "apikey"
# pass = "env(SENDGRID_API_KEY)"
# admin_email = "admin@email.com"
# sender_name = "Admin"

# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"

[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = false
# If enabled, users need to confirm their phone number before signing in.
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ .Code }}"
# Controls the minimum amount of time that must pass before sending another sms otp.
max_frequency = "5s"

# Use pre-defined map of phone number to OTP for testing.
# [auth.sms.test_otp]
# 4152127777 = "123456"

# Configure logged in session timeouts.
# [auth.sessions]
# Force log out after the specified duration.
# timebox = "24h"
# Force log out if the user has been inactive longer than the specified duration.
# inactivity_timeout = "8h"

# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"

# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"

# Multi-factor-authentication is available to Supabase Pro plan.
[auth.mfa]
# Control how many MFA factors can be enrolled at once per user.
max_enrolled_factors = 10

# Control MFA via App Authenticator (TOTP)
[auth.mfa.totp]
enroll_enabled = false
verify_enabled = false

# Configure MFA via Phone Messaging
[auth.mfa.phone]
enroll_enabled = false
verify_enabled = false
otp_length = 6
template = "Your code is {{ .Code }}"
max_frequency = "5s"

# Configure MFA via WebAuthn
# [auth.mfa.web_authn]
# enroll_enabled = true
# verify_enabled = true

# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
# Overrides the default auth redirectUrl.
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false

# Use Firebase Auth as a third-party provider alongside Supabase Auth.
[auth.third_party.firebase]
enabled = false
# project_id = "my-firebase-project"

# Use Auth0 as a third-party provider alongside Supabase Auth.
[auth.third_party.auth0]
enabled = false
# tenant = "my-auth0-tenant"
# tenant_region = "us"

# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
[auth.third_party.aws_cognito]
enabled = false
# user_pool_id = "my-user-pool-id"
# user_pool_region = "us-east-1"

[edge_runtime]
enabled = true
# Configure one of the supported request policies: `oneshot`, `per_worker`.
# Use `oneshot` for hot reload, or `per_worker` for load testing.
policy = "oneshot"
# Port to attach the Chrome inspector for debugging edge functions.
inspector_port = 8083

# Use these configurations to customize your Edge Function.
# [functions.MY_FUNCTION_NAME]
# enabled = true
# verify_jwt = true
# import_map = "./functions/MY_FUNCTION_NAME/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
# entrypoint = "./functions/MY_FUNCTION_NAME/index.ts"

[analytics]
enabled = true
port = 54327
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"

# Experimental features may be deprecated any time
[experimental]
# Configures Postgres storage engine to use OrioleDB (S3)
orioledb_version = ""
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
s3_host = "env(S3_HOST)"
# Configures S3 bucket region, eg. us-east-1
s3_region = "env(S3_REGION)"
# Configures AWS_ACCESS_KEY_ID for S3 bucket
s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"
185 changes: 185 additions & 0 deletions supabase/migrations/20250124142807_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
-- 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,
favorite_numbers int[] DEFAULT 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.';

CREATE TYPE public.todo_status AS ENUM ('NOT STARTED', 'STARTED', 'COMPLETED');
create table public.todos
(
id bigint generated by default as identity not null,
name text null,
notes text null,
done boolean null default false,
details text null,
inserted_at timestamp without time zone null default now(),
numbers int[] null,
user_id text null,
status public.todo_status not null default 'NOT STARTED'::todo_status,
constraint todos_pkey primary key (id)
) tablespace pg_default;

ALTER publication supabase_realtime add table public.todos;
alter table public.todos replica identity full;

-- 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.';

create table "public"."kitchen_sink"
(
"id" serial primary key,
"string_value" varchar(255) null,
"bool_value" BOOL DEFAULT false,
"unique_value" varchar(255) UNIQUE,
"int_value" INT null,
"float_value" FLOAT null,
"double_value" DOUBLE PRECISION null,
"datetime_value" timestamp null,
"datetime_value_1" timestamp null,
"datetime_pos_infinite_value" timestamp null,
"datetime_neg_infinite_value" timestamp null,
"list_of_strings" TEXT[] null,
"list_of_datetimes" DATE[] null,
"list_of_ints" INT[] null,
"list_of_floats" FLOAT[] null,
"int_range" INT4RANGE null
);

CREATE TABLE public.movie
(
id serial primary key,
created_at timestamp without time zone NOT NULL DEFAULT now(),
name character varying(255) NULL
);

CREATE TABLE public.person
(
id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
created_at timestamp without time zone NOT NULL DEFAULT now(),
first_name character varying(255) NULL,
last_name character varying(255) NULL
);

CREATE TABLE public.profile
(
profile_id int PRIMARY KEY references person (id),
email character varying(255) null,
created_at timestamp without time zone NOT NULL DEFAULT now()
);

CREATE TABLE public.movie_person
(
id int generated by default as identity,
movie_id int references movie (id),
person_id int references person (id),
primary key (id, movie_id, person_id)
);

insert into "public"."movie" ("created_at", "id", "name")
values ('2022-08-20 00:29:45.400188', 1, 'Top Gun: Maverick');
insert into "public"."movie" ("created_at", "id", "name")
values ('2022-08-22 00:29:45.400188', 2, 'Mad Max: Fury Road');
insert into "public"."movie" ("created_at", "id", "name")
values ('2022-08-28 00:29:45.400188', 3, 'Guns Away');


insert into "public"."person" ("created_at", "first_name", "id", "last_name")
values ('2022-08-20 00:30:02.120528', 'Tom', 1, 'Cruise');
insert into "public"."person" ("created_at", "first_name", "id", "last_name")
values ('2022-08-20 00:30:02.120528', 'Tom', 2, 'Holland');
insert into "public"."person" ("created_at", "first_name", "id", "last_name")
values ('2022-08-20 00:30:33.72443', 'Bob', 3, 'Saggett');
insert into "public"."person" ("created_at", "first_name", "id", "last_name")
values ('2022-08-20 00:30:33.72443', 'Random', 4, 'Actor');


insert into "public"."profile" ("created_at", "email", "profile_id")
values ('2022-08-20 00:30:33.72443', 'tom.cruise@supabase.io', 1);
insert into "public"."profile" ("created_at", "email", "profile_id")
values ('2022-08-20 00:30:33.72443', 'tom.holland@supabase.io', 2);
insert into "public"."profile" ("created_at", "email", "profile_id")
values ('2022-08-20 00:30:33.72443', 'bob.saggett@supabase.io', 3);

insert into "public"."movie_person" ("id", "movie_id", "person_id")
values (1, 1, 1);
insert into "public"."movie_person" ("id", "movie_id", "person_id")
values (2, 2, 2);
insert into "public"."movie_person" ("id", "movie_id", "person_id")
values (3, 1, 3);
insert into "public"."movie_person" ("id", "movie_id", "person_id")
values (4, 3, 4);


-- 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;

Unchanged files with check annotations Beta

/// <summary>
/// Untracks a client
/// </summary>
/// <param name="payload"></param>

Check warning on line 49 in Realtime/Interfaces/IRealtimePresence.cs

GitHub Actions / build-and-test

XML comment has a param tag for 'payload', but there is no parameter by that name

Check warning on line 49 in Realtime/Interfaces/IRealtimePresence.cs

GitHub Actions / build-and-test

XML comment has a param tag for 'payload', but there is no parameter by that name
/// <param name="timeoutMs"></param>

Check warning on line 50 in Realtime/Interfaces/IRealtimePresence.cs

GitHub Actions / build-and-test

XML comment has a param tag for 'timeoutMs', but there is no parameter by that name

Check warning on line 50 in Realtime/Interfaces/IRealtimePresence.cs

GitHub Actions / build-and-test

XML comment has a param tag for 'timeoutMs', but there is no parameter by that name
Task<Push> Untrack();
/// <summary>
/// <param name="args"></param>
private void HandleSocketMessage(ResponseMessage args)
{
_options.Decode!(args.Text, decoded =>

Check warning on line 430 in Realtime/RealtimeSocket.cs

GitHub Actions / build-and-test

Possible null reference argument for parameter 'arg1' in 'void Action<string, Action<SocketResponse?>>.Invoke(string arg1, Action<SocketResponse?> arg2)'.
{
Debugger.Instance.Log(this, $"Socket Message Received:\n\t{args.Text}");