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-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..eeb4cf7 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,21 @@ +on: + push: + branches: + - master + +permissions: + contents: write + pull-requests: write + issues: write + +name: release-please + +jobs: + release-please: + runs-on: ubuntu-latest + 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd21e83..d795982 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,19 @@ name: Publish NuGet Package on: - push: - branches: - - release/* # Default release branch + workflow_run: + workflows: ["release-please"] + branches: [master] + types: + - completed + +# push: +# branches: +# - release/* # Default release branch jobs: publish: + if: ${{ startsWith(github.event.head_commit.message, 'chore(main)') && github.ref == 'refs/heads/main' && github.event_name == 'push' && github.event.workflow_run.conclusion == 'success' }} name: build, pack & publish runs-on: ubuntu-latest steps: @@ -24,7 +31,7 @@ jobs: check-name: build-and-test repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 10 - + - name: Restore dependencies run: dotnet restore @@ -35,4 +42,5 @@ jobs: run: dotnet pack ./Functions/Functions.csproj --configuration Release - name: Publish on version change - run: dotnet nuget push "**/*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} + run: echo "publish on nuget" + # run: dotnet nuget push "**/*.nupkg" --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} 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..6ed1cac 100644 --- a/Functions/Client.cs +++ b/Functions/Client.cs @@ -1,16 +1,16 @@ -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; +using Supabase.Functions.Responses; [assembly: InternalsVisibleTo("FunctionsTests")] @@ -24,7 +24,7 @@ public partial class Client : IFunctionsClient /// /// 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; } @@ -45,8 +45,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 +63,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 +83,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 +106,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 +126,34 @@ private async Task HandleRequest(string url, string? token options.Headers["X-Client-Info"] = Util.GetAssemblyVersion(typeof(Client)); + if (options.FunctionRegion != FunctionRegion.Any) + { + options.Headers["x-region"] = options.FunctionRegion.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 +164,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..6958621 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,160 @@ 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; } = FunctionRegion.Any; + } + + /// + /// Define the region for requests + /// + public class FunctionRegion : IEquatable + { + private 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); + + 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/Supabase.Functions.sln b/Supabase.Functions.sln index 2bdd908..1710bd7 100644 --- a/Supabase.Functions.sln +++ b/Supabase.Functions.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml .github\workflows\build-documentation.yaml = .github\workflows\build-documentation.yaml .github\workflows\release.yml = .github\workflows\release.yml + .github\workflows\release-please.yml = .github\workflows\release-please.yml EndProjectSection EndProject Global 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" } },