An open API service indexing awesome lists of open source software.

https://github.com/manriif/supabase-edge-functions-kt

Build, serve and deploy Supabase Edge Functions with Kotlin and Gradle.
https://github.com/manriif/supabase-edge-functions-kt

edge-functions gradle-plugin intellij js kotlin kotlin-js supabase

Last synced: 3 months ago
JSON representation

Build, serve and deploy Supabase Edge Functions with Kotlin and Gradle.

Awesome Lists containing this project

README

        

[example]: https://github.com/manriif/supabase-edge-functions-kt-example
[website]: https://manriif.github.io/supabase-edge-functions-kt

# Supabase Edge Functions Kotlin

Build, serve and deploy Supabase Edge Functions with Kotlin and Gradle.

The project aims to bring the ability of writing and deploying Supabase Edge Functions using Kotlin
as primary programming language.

[![](https://img.shields.io/badge/Stability-experimental-orange)](#project-stability)
[![Kotlin](https://img.shields.io/badge/kotlin-2.0.21-blue.svg?logo=kotlin)](http://kotlinlang.org)
[![IR](https://img.shields.io/badge/Kotlin%2FJS-IR_only-yellow)](https://kotl.in/jsirsupported)
[![API](https://img.shields.io/badge/API-dokka-green)][website]
[![Maven Central](https://img.shields.io/maven-central/v/io.github.manriif.supabase-functions/gradle-plugin?label=MavenCentral&logo=apache-maven)](https://central.sonatype.com/artifact/io.github.manriif.supabase-functions/gradle-plugin)
[![Gradle Plugin](https://img.shields.io/gradle-plugin-portal/v/io.github.manriif.supabase-functions?label=Gradle&logo=gradle)](https://plugins.gradle.org/plugin/io.github.manriif.supabase-functions)
[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/)
[![slack](https://img.shields.io/badge/slack-%23supabase--kt-purple.svg?logo=slack)](https://kotlinlang.slack.com/archives/C06QXPC7064)

## Get started

It is recommended to use your favorite IntelliJ based IDE such as IntelliJ IDEA or Android Studio.

Also, it is recommended to have one gradle subproject per function.
Inspiration on how to structure your gradle project can be found in the [example][example].

### Gradle setup

If you plan to write multiple functions, declare the plugin in the root build script:

```kotlin
// /build.gradle.kts

plugins {
id("io.github.manriif.supabase-functions") version "" apply false
}
```

Apply the Gradle plugin in the build script of your project:

```kotlin
// /build.gradle.kts

plugins {
id("io.github.manriif.supabase-functions")
}

supabaseFunction {
packageName = "org.example.function" // Required, package of the main function
functionName = "my-function" // Optional, default to the project name
supabaseDir = file("supabase") // Optional, default to /supabase
envFile = file(".env.local") // Optional, default to /.env.local
projectRef = "supabase-project-ref" // Optional, no default value
importMap = false // Optional, default to true
verifyJwt = false // Optional, default to true
}
```

### Kotlin/JS setup

Apply the Kotlin Multiplatform plugin in the build script of your project:

```kotlin
// /build.gradle.kts

plugins {
id("org.jetbrains.kotlin.multiplatform")
}

kotlin {
js(IR) {
binaries.library() // Required
useEsModules() // Required
nodejs() // Required
}
}
```

### Main function

An [example][example] repository is available
to get you started faster.

The only requirement for the magic to work is to write an entry function that accepts a
single `org.w3c.fetch.Request` parameter and returns a `org.w3c.fetch.Response`.

The function can be marked with suspend modifier.

In any kotlin source file of your project (function):

```kotlin
// src/jsMain/kotlin/org/example/function/serve.kt

package org.example.function

suspend fun serve(request: Request): Response {
return Response(body = "Hello, world !")
}
```

### Run

After a successful gradle sync and if you are using an IntelliJ based IDE, you will see new run configurations for your function.



Run configurations

Run:

- ` deploy` for deploying the function to the remote project.
- ` inspect` for inspecting the javascript code with Chrome DevTools.
- ` request` for verifying the function (send preconfigured request(s)).
- ` serve` for serving the function locally.

## Features

Belows the features offered by the plugin.

| Name | ☑️ |
|---------------------------------------|-----|
| Write Kotlin code | ✅️ |
| Write Javascript code | ✅️ |
| NPM support | ✅️ |
| Multi-module support | ✅️ |
| Serve function | ✅️ |
| [Verify function](#automatic-request) | ✅️ |
| [Deploy function](#deployment) | ✅️ |
| [Import map](#import-map) | ✅️ |
| [Debugging](#debugging) | 🚧️ |

## Modules

The project provides convenient modules which covers the needs related to the development of supabase
functions.

Available modules:

- [binding-deno](modules/binding-deno/MODULE.md)
- [fetch-json](modules/fetch-json/MODULE.md)

## Advanced usage

### Continuous build

The plugin provides first class support for Gradle continuous build and configuration cache.
This results in faster builds and an uninterrupted development workflow.

Serve related tasks (serve, request, inspect) will automatically reload after file changes
are detected by gradle.

### Main function

The plugin will, by default, generate a kotlin function that acts as a bridge between your main
function and the Deno serve function. This also results in the generation of the function's `index.ts`
file.

Disable the task

If, for some reasons you do not want this behaviour, you can simply disable the related task:

```kotlin
// /build.gradle.kts

tasks {
supabaseFunctionGenerateKotlinBridge {
enabled = false
}
}
```

It is then your responsibility to connect the two worlds.

Change the main function name

By default the main function name is `serve`.
If this name struggles seducing you, you can change it by editing your function level build script.
Let's say you want your main function to be named `handleRequest`:

```kotlin
// /build.gradle.kts

tasks {
supabaseFunctionGenerateKotlinBridge {
mainFunctionName = "handleRequest"
}
}
```

After that, your main function should looks like:

```kotlin
// src/jsMain/kotlin/org/example/function/serve.kt

package org.example.function

suspend fun handleRequest(request: Request): Response {
return Response(body = "Hello, world !")
}
```

### JavaScript

You can embed local JavaScript sources from a subproject, other subproject or even through a
composite build project.

Rules

Working with JavaScript must be done according to a few rules:

- The JavaScript source code must be placed in the `src//js` of the target project.
There is no restriction regarding the kotlin source-set. It can be `commonMain`, `jsMain`, both,
or any other source-set that the `jsMain` source-set depends on.
This gives you complete flexibility on how you structure your modules.

- There cannot be the same file (same name and same path relative to the `js` directory) within two
different source-sets of the same project (module).

- There is a magical keyword `module` which must be used to refer to the local project when importing.
This keyword ensures proper resolution of js files among all included projects and depending on
the call site.

Note that this functionality relies on `import_map.json` and it is your responsibility to hand-write
these rules in case you have disabled the import map task.

Import an exported Kotlin function into a JavaScript file

```javascript
// src/jsMain/js/bonjour.js

import { howAreYou } from 'module'; // howAreYou is an exported Kotlin function

export function bonjour(name) {
return "Bonjour " + name + ", " + howAreYou();
}
```

More explanation on how to consume Kotlin code from JavaScript [here](https://kotlinlang.org/docs/js-to-kotlin-interop.html).

Import an exported JavaScript function into a Kotlin file

```kotlin
// src/jsMain/kotlin/org/example/function/Bonjour.kt
@file:JsModule("module/bonjour.js") // full path to the js file relative to the js directory after module/

package org.example.function

external fun bonjour(name: String): String
```

More explanation on how to consume Javascript code in Kotlin [here](https://kotlinlang.org/docs/js-interop.html).

### Import map

The plugin automatically configures a single import_map.json file which take cares of NPM dependencies
and local js sources files. The file is generated under the `supabase/functions` directory and aggregates
all the single `import_map.json` files of each individual function.

You can specify this import_map.json file in your favorite JavaScript IDE and it's Deno configuration.

Generate the import_map.json

The task responsible for generating the file is triggered after a successful project sync but you can manually
trigger it by running:

`./gradlew :supabaseFunctionAggregateImportMap`

Modify the generated file

You can add entries to the generated `import_map.json` by writing your own
`import_map_template.json` file under the `supabase/functions` directory.
This file will take precedence over any other `import_map.json`,
meaning that your entries will not be overwritten. This allows you to force a specific version
for an NPM package.

Do not directly modify the generated `import_map.json` as it will be overwritten.

Disable the feature

If, for some reasons you want to manually manage the import map, you can disable the related task(s):

For a single function

```kotlin
// /build.gradle.kts

supabaseFunction {
importMap = false
}

tasks {
supabaseFunctionGenerateImportMap {
enabled = false
}
}
```

For all functions

```kotlin
// /build.gradle.kts

tasks.withType {
enabled = false
}
```

Keep in mind that you should manually create and populate necessary import_map.json file(s).

### Automatic request

With the aim of limiting tools and speeding up function development time, the plugin provides the
ability to automatically send preconfigured requests to the function endpoint.

Configuration

Under the project (function) directory, create a `request-config.json` file:

```json5
{
"headers": { // Optional, defaults headers for all requests
"authorization": "Bearer ${ANON_KEY}" // ${ANON_KEY} will be resolved at runtime. You can use
// any variable printed by the `supabase status` command
},
"requests": [ // Required, list of requests that should be performed
{
"name": "Response body should be 'Hello, world !!'", // Required, the name of the request
"method": "get", // Required, the http method: get, post, put, patch, option, delete, etc
"headers": { // Optional, request headers
"authorization": "Bearer ${SERVICE_ROLE_KEY}" // Override default
},
"parameters": { // Optional, URI parameters
"name": "Paul"
},
"type": "plain", // Optional, the type of the request: `plain`, `json` or `file`
"body": "", // Conditional, body of the request, required if a type is specified
"body": "John", // Body of the request for `plain` type, must be a valid string
"body": { // Body of the request for `json` type, must be a valid json object
"from": 0,
"to": 10
},
"body": "./file-to-upload.png", // Body of the request for `file` type. File path must be
// relative to the project directory
"validation": { // Optional, used for assertions
"status": 400, // Optional, the expected response status code, default to 200
"type": "plain",// Optional, the expected response type: `plain`, `json` or `file`
"body": "", // Conditional, expected response body, required if a type is specified
"body": "Hello, world !", // Expected body for `plain` type
"body": { // Expected body for `json` type
"cities": [
{
name: "Bordeaux",
country: "France"
}
]
},
"body": "./expected-body.txt" // Body of the request for `file` type. File path must
// be relative to the project directory
}
}
]
}
```

You can further customize the behaviour of the serve task for auto request:

```kotlin
// /build.gradle.kts

tasks {
supabaseFunctionServe {
autoRequest {
logResponse = true // Print request and response details
logStatus = true // Print available supabase variables
}
}
}
```

It is also possible to pass gradle parameters for altering the behaviour and avoid modifying
gradle script:

- pass `-PsupFunLogResponse" for printing request and response details
- pass `-PsupFunLogStatus" for printing available supabase variables

And:

`./gradlew :path:to:function:supabaseFunctionServe -PsupFunLogResponse -PsubFunLogStatus`

Continuous build

When using continuous build, requests are sent after files changes are detected by gradle.
However, depending on your function size, the requests may be sent too quickly and not allow enough
time for the supabase hot loader to process the changes. This can lead to race condition issues and
results in edge function invocation error.

To solve the problem, it is possible to delay the requests sending:

```kotlin
// /build.gradle.kts

tasks {
supabaseFunctionServe {
autoRequest {
sendRequestOnCodeChangeDelay = 1000 // milliseconds, default to 500.
}
}
}
```

Note that changes to the `request-config.json` file will also trigger live reload, which let you edit
it while the task is running.

Request

You can auto request a function by running the ` request` run configuration or by running
the gradle command:

`./gradlew :path:to:function:supabaseFunctionServe -PsupFunAutoRequest`

### Debugging

Logging

Log events that are printed to the terminal window are explained [here](https://supabase.com/docs/guides/functions/logging#events-that-get-logged).
Thus, you can print your own custom log events.

For uncaught exception logs, stacktrace files are resolved relatively to your local file system.

Regarding Kotlin code, the plugin offers the possibility to map the generated javascript file to the
Kotlin source file to facilitate debugging. On the other hand, this may not be as accurate, especially
because of inlining and suspension. That's why this feature is marked as experimental.

To apply source mapping:

```kotlin
// /build.gradle.kts

tasks {
supabaseFunctionServe {
@OptIn(ExperimentalSupabaseFunctionApi::class)
stackTraceSourceMapStrategy = StackTraceSourceMapStrategy.KotlinPreferred
// or if you don't want to hear about js
stackTraceSourceMapStrategy = StackTraceSourceMapStrategy.KotlinOnly
}
}
```

JavaScript code

It is possible to use Chrome DevTools for JavaScript debugging as specified [here](https://supabase.com/docs/guides/functions/debugging-tools).
By default, the inspect mode is `brk`, if you want to change it:

```kotlin
// /build.gradle.kts

tasks {
supabaseFunctionServe {
inspect {
mode = ServeInspect.Mode.Wait // default to ServeInspect.Mode.Brk
main = true // create an inspector session for the main worker, default to false
debug = true // pass --debug flag to the serve command, default to false
}
}
}
```

Kotlin code 🚧️

Currently it is not possible to debug Kotlin code.
This is the project's next major feature.

As this is not a trivial task and due to lack of time, it may take some time before such a feature
is released. The feature would likely take the form of an IDEA plugin because this goes beyond the
scope of a gradle plugin.

Inspect

You can inspect a function by running the ` inspect` run configuration or by running
the gradle command:

`./gradlew :path:to:function:supabaseFunctionServe -PsupFunInspect`

### Run configurations

Run configurations, for each function, are automatically created for IntelliJ based IDEs.

Configure

You can choose which run configuration to generate:

```kotlin
// /build.gradle.kts

supabaseFunction {
runConfiguration {
deploy = false // Enable the deploy run configuration, true by default

serve { // Serve run configuration
enabled = false // Enable the configuration, true by default
continuous = false // continuous build enabled by default, true by default
}

inspect { // Inspect run configuration
enabled = false // Enable the configuration, true by default
continuous = false // continuous build enabled by default, true by default
}

request { // Request run configuration
enabled = false // Enable the configuration, true by default
continuous = false // continuous build enabled by default, true by default
}
}
}
```

### Deployment

Function can be deployed to the remote project from the plugin.

Deploy

You can deploy a function by running the ` deploy` run configuration or by running
the gradle command:

`./gradlew :path:to:function:supabaseFunctionDeploy`

Before deploying the function, make sure you have correctly [linked](https://supabase.com/docs/reference/cli/supabase-link)
the remote project.

### Gitignore

It is generally a good practice not to import files that are generated to VCS. Thus, and by its nature,
the plugin provides a task for creating or updating necessary `.gitignore` files. Existing `.gitignore `
files will not be overwritten but completed with missing entries.

Disable or edit the task

You can disable the task or change its behaviour at the project level:

```kotlin
// /build.gradle.kts

tasks {
supabaseFunctionServe {
enabled = false // Disable the task
importMapEntry = false // Prevent the task from adding the import_map.json to .gitignore
// This could be necessary if you manually configured the import map
indexEntry = false // Prevent the task from adding the index.ts to .gitignore
// This could be necessary if you manually created the index.ts file
}
}
```

## Project stability

The project is currently in an experimental phase due to its freshness and reliance on
experimental features such as [Kotlin JsExport](https://kotlinlang.org/docs/js-to-kotlin-interop.html#jsexport-annotation).

It should therefore be consumed in moderation.

## Limitations

Following limitations applies:

- Kotlin versions before 2.0 are not supported
- browser JS subtarged is not supported
- `per-file` and `whole-program` JS IR [output granularity](https://kotlinlang.org/docs/js-ir-compiler.html#output-mode) are not supported.
- Depending on a Kotlin library that uses require() may result in runtime error