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 + +[![Build and Test](https://github.com/supabase-community/functions-csharp/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/supabase-community/functions-csharp/actions/workflows/build-and-test.yml) +[![NuGet](https://img.shields.io/nuget/vpre/Supabase.Functions)](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" } },