diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 401338e..d31e317 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -24,8 +24,15 @@ jobs:
- name: Build
run: dotnet build --configuration Release --no-restore
- - name: Initialize Testing Stack
- run: docker-compose up -d
+ - uses: supabase/setup-cli@v1
+ with:
+ version: latest
+
+ - name: Start supabase
+ run: supabase start
+
+# - name: Initialize Testing Stack
+# run: docker-compose up -d
- name: Test
run: dotnet test --no-restore
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cd21e83..106d39f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,12 +1,27 @@
-name: Publish NuGet Package
+name: Release - Publish NuGet Package
on:
push:
branches:
- - release/* # Default release branch
+ - master
jobs:
+ release-please:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ issues: write
+ steps:
+ - uses: googleapis/release-please-action@v4
+ with:
+ target-branch: ${{ github.ref_name }}
+ manifest-file: .release-please-manifest.json
+ config-file: release-please-config.json
+
publish:
+ needs: release-please
+ if: ${{ github.repository_owner == 'supabase-community' && startsWith(github.event.head_commit.message, 'chore(master)') && github.ref == 'refs/heads/master' && github.event_name == 'push' }}
name: build, pack & publish
runs-on: ubuntu-latest
steps:
@@ -24,7 +39,7 @@ jobs:
check-name: build-and-test
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
-
+
- name: Restore dependencies
run: dotnet restore
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..895bf0e
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "2.0.0"
+}
diff --git a/Functions/Client.cs b/Functions/Client.cs
index 16dc389..d158e72 100644
--- a/Functions/Client.cs
+++ b/Functions/Client.cs
@@ -1,16 +1,15 @@
-using Newtonsoft.Json;
-using Supabase.Core;
-using Supabase.Core.Extensions;
-using Supabase.Functions.Interfaces;
-using Supabase.Functions.Responses;
-using System;
+using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Web;
+using Newtonsoft.Json;
+using Supabase.Core;
+using Supabase.Core.Extensions;
using Supabase.Functions.Exceptions;
+using Supabase.Functions.Interfaces;
[assembly: InternalsVisibleTo("FunctionsTests")]
@@ -21,10 +20,11 @@ public partial class Client : IFunctionsClient
{
private HttpClient _httpClient = new HttpClient();
private readonly string _baseUrl;
+ private readonly FunctionRegion _region;
///
/// Function that can be set to return dynamic headers.
- ///
+ ///
/// Headers specified in the method parameters will ALWAYS take precedence over headers returned by this function.
///
public Func>? GetHeaders { get; set; }
@@ -33,9 +33,11 @@ public partial class Client : IFunctionsClient
/// Initializes a functions client
///
///
- public Client(string baseUrl)
+ ///
+ public Client(string baseUrl, FunctionRegion? region = null)
{
_baseUrl = baseUrl;
+ _region = region ?? FunctionRegion.Any;
}
///
@@ -45,8 +47,11 @@ public Client(string baseUrl)
/// Anon Key.
/// Options
///
- public async Task RawInvoke(string functionName, string? token = null,
- InvokeFunctionOptions? options = null)
+ public async Task RawInvoke(
+ string functionName,
+ string? token = null,
+ InvokeFunctionOptions? options = null
+ )
{
var url = $"{_baseUrl}/{functionName}";
@@ -60,8 +65,11 @@ public async Task RawInvoke(string functionName, string? token = nu
/// Anon Key.
/// Options
///
- public async Task Invoke(string functionName, string? token = null,
- InvokeFunctionOptions? options = null)
+ public async Task Invoke(
+ string functionName,
+ string? token = null,
+ InvokeFunctionOptions? options = null
+ )
{
var url = $"{_baseUrl}/{functionName}";
var response = await HandleRequest(url, token, options);
@@ -77,8 +85,12 @@ public async Task Invoke(string functionName, string? token = null,
/// Anon Key.
/// Options
///
- public async Task Invoke(string functionName, string? token = null,
- InvokeFunctionOptions? options = null) where T : class
+ public async Task Invoke(
+ string functionName,
+ string? token = null,
+ InvokeFunctionOptions? options = null
+ )
+ where T : class
{
var url = $"{_baseUrl}/{functionName}";
var response = await HandleRequest(url, token, options);
@@ -96,8 +108,11 @@ public async Task Invoke(string functionName, string? token = null,
///
///
///
- private async Task HandleRequest(string url, string? token = null,
- InvokeFunctionOptions? options = null)
+ private async Task HandleRequest(
+ string url,
+ string? token = null,
+ InvokeFunctionOptions? options = null
+ )
{
options ??= new InvokeFunctionOptions();
@@ -113,26 +128,40 @@ private async Task HandleRequest(string url, string? token
options.Headers["X-Client-Info"] = Util.GetAssemblyVersion(typeof(Client));
+ var region = options.FunctionRegion;
+ if (region == null)
+ {
+ region = _region;
+ }
+
+ if (region != FunctionRegion.Any)
+ {
+ options.Headers["x-region"] = region.ToString();
+ }
+
var builder = new UriBuilder(url);
var query = HttpUtility.ParseQueryString(builder.Query);
builder.Query = query.ToString();
- using var requestMessage = new HttpRequestMessage(HttpMethod.Post, builder.Uri);
- requestMessage.Content = new StringContent(JsonConvert.SerializeObject(options.Body), Encoding.UTF8,
- "application/json");
+ using var requestMessage = new HttpRequestMessage(options.HttpMethod, builder.Uri);
+ requestMessage.Content = new StringContent(
+ JsonConvert.SerializeObject(options.Body),
+ Encoding.UTF8,
+ "application/json"
+ );
foreach (var kvp in options.Headers)
{
requestMessage.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
}
-
+
if (_httpClient.Timeout != options.HttpTimeout)
{
_httpClient = new HttpClient();
_httpClient.Timeout = options.HttpTimeout;
}
-
+
var response = await _httpClient.SendAsync(requestMessage);
if (response.IsSuccessStatusCode && !response.Headers.Contains("x-relay-error"))
@@ -143,10 +172,10 @@ private async Task HandleRequest(string url, string? token
{
Content = content,
Response = response,
- StatusCode = (int)response.StatusCode
+ StatusCode = (int)response.StatusCode,
};
exception.AddReason();
throw exception;
}
}
-}
\ No newline at end of file
+}
diff --git a/Functions/Functions.csproj b/Functions/Functions.csproj
index eec4ade..4108d70 100644
--- a/Functions/Functions.csproj
+++ b/Functions/Functions.csproj
@@ -16,8 +16,10 @@
https://avatars.githubusercontent.com/u/54469796?s=200&v=4
https://github.com/supabase-community/functions-csharp
supabase, functions
+
2.0.0
2.0.0
+
true
icon.png
README.md
@@ -31,7 +33,7 @@
- 2.0.0
+ 2.0.0
$(VersionPrefix)-$(VersionSuffix)
$(VersionPrefix)
@@ -46,4 +48,4 @@
-
\ No newline at end of file
+
diff --git a/Functions/InvokeFunctionOptions.cs b/Functions/InvokeFunctionOptions.cs
index b4252f4..cf27f1f 100644
--- a/Functions/InvokeFunctionOptions.cs
+++ b/Functions/InvokeFunctionOptions.cs
@@ -1,6 +1,7 @@
using System;
-using Newtonsoft.Json;
using System.Collections.Generic;
+using System.Net.Http;
+using Newtonsoft.Json;
namespace Supabase.Functions
{
@@ -8,7 +9,7 @@ public partial class Client
{
///
/// Options that can be supplied to a function invocation.
- ///
+ ///
/// Note: If Headers.Authorization is set, it can be later overriden if a token is supplied in the method call.
///
public class InvokeFunctionOptions
@@ -16,8 +17,8 @@ public class InvokeFunctionOptions
///
/// Headers to be included on the request.
///
- public Dictionary Headers { get; set; } = new Dictionary();
-
+ public Dictionary Headers { get; set; } =
+ new Dictionary();
///
/// Body of the Request
@@ -30,6 +31,164 @@ public class InvokeFunctionOptions
/// https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=net-8.0#remarks
///
public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(100);
+
+ ///
+ /// Http method of the Request
+ ///
+ public HttpMethod HttpMethod { get; set; } = HttpMethod.Post;
+
+ ///
+ /// Region of the request
+ ///
+ public FunctionRegion? FunctionRegion { get; set; } = null;
+ }
+
+ ///
+ /// Define the region for requests
+ ///
+ public class FunctionRegion : IEquatable
+ {
+ private readonly string _region;
+
+ ///
+ /// Empty region
+ ///
+ public static FunctionRegion Any { get; } = new FunctionRegion("any");
+
+ ///
+ /// Represents the region "ap-northeast-1" for function requests.
+ ///
+ public static FunctionRegion ApNortheast1 { get; } =
+ new FunctionRegion("ap-northeast-1");
+
+ ///
+ /// Represents the "ap-northeast-2" region for function invocation.
+ ///
+ public static FunctionRegion ApNortheast2 { get; } =
+ new FunctionRegion("ap-northeast-2");
+
+ ///
+ /// Represents the "ap-south-1" region used for requests.
+ ///
+ public static FunctionRegion ApSouth1 { get; } = new FunctionRegion("ap-south-1");
+
+ ///
+ /// Represents the region "ap-southeast-1" for function invocation.
+ ///
+ public static FunctionRegion ApSoutheast1 { get; } =
+ new FunctionRegion("ap-southeast-1");
+
+ ///
+ /// Represents the "ap-southeast-2" region for requests.
+ ///
+ public static FunctionRegion ApSoutheast2 { get; } =
+ new FunctionRegion("ap-southeast-2");
+
+ ///
+ /// Represents the Canada (Central) region for requests.
+ ///
+ public static FunctionRegion CaCentral1 { get; } = new FunctionRegion("ca-central-1");
+
+ ///
+ /// Represents the "eu-central-1" region for function invocation.
+ ///
+ public static FunctionRegion EuCentral1 { get; } = new FunctionRegion("eu-central-1");
+
+ ///
+ /// Represents the "eu-west-1" function region for requests.
+ ///
+ public static FunctionRegion EuWest1 { get; } = new FunctionRegion("eu-west-1");
+
+ ///
+ /// Represents the "eu-west-2" region for function invocation requests.
+ ///
+ public static FunctionRegion EuWest2 { get; } = new FunctionRegion("eu-west-2");
+
+ ///
+ /// Represents the AWS region 'eu-west-3'.
+ ///
+ public static FunctionRegion EuWest3 { get; } = new FunctionRegion("eu-west-3");
+
+ ///
+ /// Represents the South America (São Paulo) region for requests.
+ ///
+ public static FunctionRegion SaEast1 { get; } = new FunctionRegion("sa-east-1");
+
+ ///
+ /// Represents the "us-east-1" region for function requests.
+ ///
+ public static FunctionRegion UsEast1 { get; } = new FunctionRegion("us-east-1");
+
+ ///
+ /// Represents the us-west-1 region for function requests.
+ ///
+ public static FunctionRegion UsWest1 { get; } = new FunctionRegion("us-west-1");
+
+ ///
+ /// Represents the "us-west-2" region for requests.
+ ///
+ public static FunctionRegion UsWest2 { get; } = new FunctionRegion("us-west-2");
+
+ ///
+ /// Define the region for requests
+ ///
+ public FunctionRegion(string region)
+ {
+ _region = region;
+ }
+
+ ///
+ /// Check if the object is identical to the reference passed
+ ///
+ public override bool Equals(object obj)
+ {
+ return obj is FunctionRegion r && Equals(r);
+ }
+
+ ///
+ /// Generate Hash code
+ ///
+ public override int GetHashCode()
+ {
+ return _region.GetHashCode();
+ }
+
+ ///
+ /// Check if the object is identical to the reference passed
+ ///
+ public bool Equals(FunctionRegion other)
+ {
+ return _region == other._region;
+ }
+
+ ///
+ /// Overloading the operator ==
+ ///
+ public static bool operator ==(FunctionRegion? left, FunctionRegion? right) =>
+ Equals(left, right);
+
+ ///
+ /// Overloading the operator !=
+ ///
+ public static bool operator !=(FunctionRegion? left, FunctionRegion? right) =>
+ !Equals(left, right);
+
+ ///
+ /// Overloads the explicit cast operator to convert a FunctionRegion object to a string.
+ ///
+ public static explicit operator string(FunctionRegion region) => region.ToString();
+
+ ///
+ /// Overloads the explicit cast operator to convert a string to a FunctionRegion object.
+ ///
+ public static explicit operator FunctionRegion(string region) =>
+ new FunctionRegion(region);
+
+ ///
+ /// Returns a string representation of the FunctionRegion instance.
+ ///
+ /// A string that represents the current FunctionRegion instance.
+ public override string ToString() => _region;
}
}
-}
\ No newline at end of file
+}
diff --git a/FunctionsTests/ClientTests.cs b/FunctionsTests/ClientTests.cs
index 06c4a8a..78bbbcd 100644
--- a/FunctionsTests/ClientTests.cs
+++ b/FunctionsTests/ClientTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
+using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
@@ -19,8 +20,8 @@ public class ClientTests
[TestInitialize]
public void Initialize()
{
- _token = GenerateToken("37c304f8-51aa-419a-a1af-06154e63707a");
- _client = new Client("http://localhost:9000");
+ _token = GenerateToken("super-secret-jwt-token-with-at-least-32-characters-long");
+ _client = new Client("http://localhost:54321/functions/v1");
}
[TestMethod("Invokes a function.")]
@@ -28,41 +29,57 @@ public async Task Invokes()
{
const string function = "hello";
- var result = await _client.Invoke(function, _token, new InvokeFunctionOptions
- {
- Body = new Dictionary
+ var result = await _client.Invoke(
+ function,
+ _token,
+ new InvokeFunctionOptions
{
- {"name", "supabase" }
+ Body = new Dictionary { { "name", "supabase" } },
+ HttpMethod = HttpMethod.Post,
}
- });
+ );
Assert.IsTrue(result.Contains("supabase"));
-
- var result2 = await _client.Invoke>(function, _token, new InvokeFunctionOptions
- {
- Body = new Dictionary
+ var result2 = await _client.Invoke>(
+ function,
+ _token,
+ new InvokeFunctionOptions
{
- { "name", "functions" }
+ Body = new Dictionary { { "name", "functions" } },
+ HttpMethod = HttpMethod.Post,
}
- });
+ );
Assert.IsInstanceOfType(result2, typeof(Dictionary));
Assert.IsTrue(result2.ContainsKey("message"));
Assert.IsTrue(result2["message"].Contains("functions"));
-
- var result3 = await _client.RawInvoke(function, _token, new InvokeFunctionOptions
- {
- Body = new Dictionary
+ var result3 = await _client.RawInvoke(
+ function,
+ _token,
+ new InvokeFunctionOptions
{
- { "name", "functions" }
+ Body = new Dictionary { { "name", "functions" } },
+ HttpMethod = HttpMethod.Post,
}
- });
+ );
var bytes = await result3.ReadAsByteArrayAsync();
Assert.IsInstanceOfType(bytes, typeof(byte[]));
+
+ var result4 = await _client.Invoke(
+ function,
+ _token,
+ new InvokeFunctionOptions
+ {
+ Body = [],
+ HttpMethod = HttpMethod.Get,
+ }
+ );
+
+ Assert.IsTrue(result4.Contains(function));
}
private static string GenerateToken(string secret)
@@ -71,7 +88,10 @@ private static string GenerateToken(string secret)
var tokenDescriptor = new SecurityTokenDescriptor
{
- SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature)
+ SigningCredentials = new SigningCredentials(
+ signingKey,
+ SecurityAlgorithms.HmacSha256Signature
+ ),
};
var tokenHandler = new JwtSecurityTokenHandler();
@@ -79,4 +99,4 @@ private static string GenerateToken(string secret)
return tokenHandler.WriteToken(securityToken);
}
}
-}
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index ce7bf1d..27995c4 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,7 @@
-
-
-
-
-
-
-
-
-
-
+# Supabase.Functions
+
+[](https://github.com/supabase-community/functions-csharp/actions/workflows/build-and-test.yml)
+[](https://www.nuget.com/packages/Supabase.Functions/)
---
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..d93bd7d
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,16 @@
+{
+ "packages": {
+ ".": {
+ "changelog-path": "CHANGELOG.md",
+ "bump-minor-pre-major": false,
+ "bump-patch-for-minor-pre-major": false,
+ "draft": false,
+ "prerelease": false,
+ "release-type": "simple",
+ "extra-files": [
+ "Functions/Functions.csproj"
+ ]
+ }
+ },
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
+}
diff --git a/supabase/functions/hello/index.ts b/supabase/functions/hello/index.ts
index 820653e..09b19bb 100644
--- a/supabase/functions/hello/index.ts
+++ b/supabase/functions/hello/index.ts
@@ -6,12 +6,19 @@ import { serve } from "https://deno.land/std@0.131.0/http/server.ts"
console.log("Hello from Functions!")
-serve(async (req) => {
- const { name } = await req.json()
+serve(async (req: Request) => {
+ let value = req.url.substring(req.url.lastIndexOf("/") + 1)
+ if (req.body != null) {
+ const { name } = await req.json()
+ value = name
+ }
+
const data = {
- message: `Hello ${name}!`,
+ message: `Hello ${value}!`,
}
+ console.log("response", JSON.stringify(data))
+
return new Response(
JSON.stringify(data),
{ headers: { "Content-Type": "application/json" } },