Skip to content

Commit 693b7d3

Browse files
feat(functions) update functions template to match new 'server' style (#5063)
1 parent e4a7ace commit 693b7d3

9 files changed

Lines changed: 199 additions & 44 deletions

File tree

cmd/functions.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ var (
8585
},
8686
}
8787

88+
authMode = utils.EnumFlag{
89+
Allowed: []string{
90+
string(new_.AuthModeNone),
91+
string(new_.AuthModeApiKey),
92+
string(new_.AuthModeUser),
93+
},
94+
Value: string(new_.AuthModeApiKey),
95+
}
8896
functionsNewCmd = &cobra.Command{
8997
Use: "new <Function name>",
9098
Short: "Create a new Function locally",
@@ -94,7 +102,9 @@ var (
94102
return cmd.Root().PersistentPreRunE(cmd, args)
95103
},
96104
RunE: func(cmd *cobra.Command, args []string) error {
97-
return new_.Run(cmd.Context(), args[0], afero.NewOsFs())
105+
authMode := new_.AuthMode(authMode.Value)
106+
107+
return new_.Run(cmd.Context(), args[0], authMode, afero.NewOsFs())
98108
},
99109
}
100110

@@ -172,6 +182,7 @@ func init() {
172182
functionsDownloadCmd.MarkFlagsMutuallyExclusive("use-api", "use-docker", "legacy-bundle")
173183
cobra.CheckErr(downloadFlags.MarkHidden("legacy-bundle"))
174184
cobra.CheckErr(downloadFlags.MarkHidden("use-docker"))
185+
functionsNewCmd.Flags().Var(&authMode, "auth", "use a specific auth mode")
175186
functionsCmd.AddCommand(functionsListCmd)
176187
functionsCmd.AddCommand(functionsDeleteCmd)
177188
functionsCmd.AddCommand(functionsDeployCmd)

internal/functions/new/new.go

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,49 @@ import (
1616
"github.com/supabase/cli/internal/utils/flags"
1717
)
1818

19+
type AuthMode string
20+
21+
const (
22+
AuthModeNone AuthMode = "none"
23+
AuthModeApiKey AuthMode = "apikey"
24+
AuthModeUser AuthMode = "user"
25+
)
26+
1927
var (
20-
//go:embed templates/index.ts
21-
indexEmbed string
28+
//go:embed templates/index_auth_mode_none.ts
29+
indexAuthModeNoneEmbed string
30+
//go:embed templates/index_auth_mode_apikey.ts
31+
indexAuthModeApiKeyEmbed string
32+
//go:embed templates/index_auth_mode_user.ts
33+
indexAuthModeUserEmbed string
34+
2235
//go:embed templates/deno.json
2336
denoEmbed string
2437
//go:embed templates/.npmrc
2538
npmrcEmbed string
2639
//go:embed templates/config.toml
2740
configEmbed string
2841

29-
indexTemplate = template.Must(template.New("index").Parse(indexEmbed))
42+
indexAuthTemplates = map[AuthMode]*template.Template{
43+
AuthModeNone: template.Must(template.New("index").Parse(indexAuthModeNoneEmbed)),
44+
AuthModeApiKey: template.Must(template.New("index").Parse(indexAuthModeApiKeyEmbed)),
45+
AuthModeUser: template.Must(template.New("index").Parse(indexAuthModeUserEmbed)),
46+
}
47+
3048
configTemplate = template.Must(template.New("config").Parse(configEmbed))
3149
)
3250

3351
type indexConfig struct {
34-
URL string
35-
Token string
52+
URL string
53+
PublishableKey string
54+
}
55+
56+
type functionConfig struct {
57+
Slug string
58+
VerifyJWT bool
3659
}
3760

38-
func Run(ctx context.Context, slug string, fsys afero.Fs) error {
61+
func Run(ctx context.Context, slug string, authMode AuthMode, fsys afero.Fs) error {
3962
// 1. Sanity checks.
4063
if err := utils.ValidateFunctionSlug(slug); err != nil {
4164
return err
@@ -56,17 +79,18 @@ func Run(ctx context.Context, slug string, fsys afero.Fs) error {
5679
if err := flags.LoadConfig(fsys); err != nil {
5780
fmt.Fprintln(utils.GetDebugLogger(), err)
5881
}
59-
if err := createEntrypointFile(slug, fsys); err != nil {
82+
if err := createEntrypointFile(slug, authMode, fsys); err != nil {
6083
return err
6184
}
62-
if err := appendConfigFile(slug, fsys); err != nil {
85+
verifyJWT := authMode == AuthModeUser
86+
if err := appendConfigFile(slug, verifyJWT, fsys); err != nil {
6387
return err
6488
}
6589
// 3. Create optional files
66-
if err := afero.WriteFile(fsys, filepath.Join(funcDir, "deno.json"), []byte(denoEmbed), 0644); err != nil {
90+
if err := afero.WriteFile(fsys, filepath.Join(funcDir, "deno.json"), []byte(denoEmbed), 0o644); err != nil {
6791
return errors.Errorf("failed to create deno.json config: %w", err)
6892
}
69-
if err := afero.WriteFile(fsys, filepath.Join(funcDir, ".npmrc"), []byte(npmrcEmbed), 0644); err != nil {
93+
if err := afero.WriteFile(fsys, filepath.Join(funcDir, ".npmrc"), []byte(npmrcEmbed), 0o644); err != nil {
7094
return errors.Errorf("failed to create .npmrc config: %w", err)
7195
}
7296
fmt.Println("Created new Function at " + utils.Bold(funcDir))
@@ -79,33 +103,40 @@ func Run(ctx context.Context, slug string, fsys afero.Fs) error {
79103
return nil
80104
}
81105

82-
func createEntrypointFile(slug string, fsys afero.Fs) error {
106+
func createEntrypointFile(slug string, authMode AuthMode, fsys afero.Fs) error {
83107
entrypointPath := filepath.Join(utils.FunctionsDir, slug, "index.ts")
84-
f, err := fsys.OpenFile(entrypointPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
108+
f, err := fsys.OpenFile(entrypointPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
85109
if err != nil {
86110
return errors.Errorf("failed to create entrypoint: %w", err)
87111
}
88112
defer f.Close()
113+
indexTemplate, hasTemplate := indexAuthTemplates[authMode]
114+
if !hasTemplate {
115+
return errors.Errorf("failed to write entrypoint: '%v' is not a valid template", authMode)
116+
}
89117
if err := indexTemplate.Option("missingkey=error").Execute(f, indexConfig{
90-
URL: utils.GetApiUrl("/functions/v1/" + slug),
91-
Token: utils.Config.Auth.AnonKey.Value,
118+
URL: utils.GetApiUrl("/functions/v1/" + slug),
119+
PublishableKey: utils.Config.Auth.PublishableKey.Value,
92120
}); err != nil {
93121
return errors.Errorf("failed to write entrypoint: %w", err)
94122
}
95123
return nil
96124
}
97125

98-
func appendConfigFile(slug string, fsys afero.Fs) error {
126+
func appendConfigFile(slug string, verifyJWT bool, fsys afero.Fs) error {
99127
if _, exists := utils.Config.Functions[slug]; exists {
100128
fmt.Fprintf(os.Stderr, "[functions.%s] is already declared in %s\n", slug, utils.Bold(utils.ConfigPath))
101129
return nil
102130
}
103-
f, err := fsys.OpenFile(utils.ConfigPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
131+
f, err := fsys.OpenFile(utils.ConfigPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
104132
if err != nil {
105133
return errors.Errorf("failed to append config: %w", err)
106134
}
107135
defer f.Close()
108-
if err := configTemplate.Option("missingkey=error").Execute(f, slug); err != nil {
136+
if err := configTemplate.Option("missingkey=error").Execute(f, functionConfig{
137+
Slug: slug,
138+
VerifyJWT: verifyJWT,
139+
}); err != nil {
109140
return errors.Errorf("failed to append template: %w", err)
110141
}
111142
return nil

internal/functions/new/new_test.go

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package new
22

33
import (
44
"context"
5+
"fmt"
56
"path/filepath"
67
"testing"
78

@@ -16,7 +17,7 @@ func TestNewCommand(t *testing.T) {
1617
// Setup in-memory fs
1718
fsys := afero.NewMemMapFs()
1819
// Run test
19-
assert.NoError(t, Run(context.Background(), "test-func", fsys))
20+
assert.NoError(t, Run(context.Background(), "test-func", AuthModeNone, fsys))
2021
// Validate output
2122
funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts")
2223
content, err := afero.ReadFile(fsys, funcPath)
@@ -26,8 +27,11 @@ func TestNewCommand(t *testing.T) {
2627
)
2728

2829
// Verify config.toml is updated
29-
_, err = afero.ReadFile(fsys, utils.ConfigPath)
30+
content, err = afero.ReadFile(fsys, utils.ConfigPath)
3031
assert.NoError(t, err, "config.toml should be created")
32+
assert.Contains(t, string(content), "[functions.test-func]")
33+
// Always access mode should not verify jwt
34+
assert.Contains(t, string(content), "verify_jwt = false")
3135

3236
// Verify deno.json exists
3337
denoPath := filepath.Join(utils.FunctionsDir, "test-func", "deno.json")
@@ -40,23 +44,54 @@ func TestNewCommand(t *testing.T) {
4044
assert.NoError(t, err, ".npmrc should be created")
4145
})
4246

47+
t.Run("creates new function with apikey access", func(t *testing.T) {
48+
fsys := afero.NewMemMapFs()
49+
assert.NoError(t, Run(context.Background(), "test-func", AuthModeApiKey, fsys))
50+
51+
// Validate output
52+
funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts")
53+
content, _ := afero.ReadFile(fsys, funcPath)
54+
// Should contain the PublishableKey as example
55+
assert.Contains(t, string(content), fmt.Sprintf("--header 'apiKey: %v'", utils.Config.Auth.PublishableKey.Value))
56+
57+
// Verify config.toml is updated to not verify jwt
58+
content, _ = afero.ReadFile(fsys, utils.ConfigPath)
59+
assert.Contains(t, string(content), "verify_jwt = false")
60+
})
61+
62+
t.Run("creates new function with user access", func(t *testing.T) {
63+
fsys := afero.NewMemMapFs()
64+
assert.NoError(t, Run(context.Background(), "test-func", AuthModeUser, fsys))
65+
66+
// Validate output
67+
funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts")
68+
content, _ := afero.ReadFile(fsys, funcPath)
69+
// Should contain the PublishableKey as example as well placeholder for UserToken
70+
assert.Contains(t, string(content), fmt.Sprintf("--header 'apiKey: %v'", utils.Config.Auth.PublishableKey.Value))
71+
assert.Contains(t, string(content), "--header 'Authorization: Bearer <UserToken>'")
72+
73+
// Verify config.toml is updated and verify jwt enabled
74+
content, _ = afero.ReadFile(fsys, utils.ConfigPath)
75+
assert.Contains(t, string(content), "verify_jwt = true")
76+
})
77+
4378
t.Run("throws error on malformed slug", func(t *testing.T) {
44-
assert.Error(t, Run(context.Background(), "@", afero.NewMemMapFs()))
79+
assert.Error(t, Run(context.Background(), "@", AuthModeNone, afero.NewMemMapFs()))
4580
})
4681

4782
t.Run("throws error on duplicate slug", func(t *testing.T) {
4883
// Setup in-memory fs
4984
fsys := afero.NewMemMapFs()
5085
funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts")
51-
require.NoError(t, afero.WriteFile(fsys, funcPath, []byte{}, 0644))
86+
require.NoError(t, afero.WriteFile(fsys, funcPath, []byte{}, 0o644))
5287
// Run test
53-
assert.Error(t, Run(context.Background(), "test-func", fsys))
88+
assert.Error(t, Run(context.Background(), "test-func", AuthModeNone, fsys))
5489
})
5590

5691
t.Run("throws error on permission denied", func(t *testing.T) {
5792
// Setup in-memory fs
5893
fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
5994
// Run test
60-
assert.Error(t, Run(context.Background(), "test-func", fsys))
95+
assert.Error(t, Run(context.Background(), "test-func", AuthModeNone, fsys))
6196
})
6297
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11

2-
[functions.{{ . }}]
2+
[functions.{{ .Slug }}]
33
enabled = true
4-
verify_jwt = true
5-
import_map = "./functions/{{ . }}/deno.json"
4+
verify_jwt = {{ .VerifyJWT }}
5+
import_map = "./functions/{{ .Slug }}/deno.json"
66
# Uncomment to specify a custom file path to the entrypoint.
77
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
8-
entrypoint = "./functions/{{ . }}/index.ts"
8+
entrypoint = "./functions/{{ .Slug }}/index.ts"
99
# Specifies static files to be bundled with the function. Supports glob patterns.
1010
# For example, if you want to serve static HTML pages in your function:
11-
# static_files = [ "./functions/{{ . }}/*.html" ]
11+
# static_files = [ "./functions/{{ .Slug }}/*.html" ]
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"imports": {
3-
"@supabase/functions-js": "jsr:@supabase/functions-js@^2"
3+
"@supabase/functions-js": "jsr:@supabase/functions-js@^2",
4+
"@supabase/server": "npm:@supabase/server@^1"
45
}
56
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Follow this setup guide to integrate the Deno language server with your editor:
2+
// https://deno.land/manual/getting_started/setup_your_environment
3+
// This enables autocomplete, go to definition, etc.
4+
5+
// Setup type definitions for built-in Supabase Runtime APIs
6+
import "@supabase/functions-js/edge-runtime.d.ts";
7+
import { withSupabase } from "@supabase/server";
8+
9+
console.log("Hello from Functions!");
10+
11+
// This endpoint uses 'publishable' | 'secret' access, apiKey is required.
12+
// Use publishable for Client-facing, key-validated endpoints
13+
// Use secret for Server-to-server, internal calls
14+
export default {
15+
fetch: withSupabase({ auth: ["publishable", "secret"] }, async (req, ctx) => {
16+
// Called by another service with a secret key
17+
// ctx.supabaseAdmin bypasses RLS — use for privileged operations
18+
/*
19+
if (ctx.authType === "secret") {
20+
const { user_id } = await req.json();
21+
const { data } = await ctx.supabaseAdmin.auth.admin.getUserById(user_id);
22+
23+
return Response.json({
24+
email: data?.user?.email,
25+
});
26+
}
27+
*/
28+
29+
const { name } = await req.json();
30+
31+
return Response.json({
32+
message: `Hello ${name}!`,
33+
});
34+
}),
35+
};
36+
37+
/* To invoke locally:
38+
39+
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
40+
2. Make an HTTP request:
41+
42+
curl -i --location --request POST '{{ .URL }}' \
43+
--header 'apiKey: {{ .PublishableKey }}' \
44+
--data '{"name":"Functions"}'
45+
46+
*/
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Follow this setup guide to integrate the Deno language server with your editor:
2+
// https://deno.land/manual/getting_started/setup_your_environment
3+
// This enables autocomplete, go to definition, etc.
4+
5+
// Setup type definitions for built-in Supabase Runtime APIs
6+
import "@supabase/functions-js/edge-runtime.d.ts";
7+
import { withSupabase } from "@supabase/server";
8+
9+
console.log("Hello from Functions!");
10+
11+
// This endpoint uses auth 'none', no credentials required, every request is accepted.
12+
// Use it for health checks, public APIs, or when you need to implement your own auth logic.
13+
export default {
14+
fetch: withSupabase({ auth: "none" }, async (req, ctx) => {
15+
const { name } = await req.json();
16+
17+
return Response.json({
18+
message: `Hello ${name}!`,
19+
});
20+
}),
21+
};
22+
23+
/* To invoke locally:
24+
25+
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
26+
2. Make an HTTP request:
27+
28+
curl -i --location --request POST '{{ .URL }}' \
29+
--header 'Content-Type: application/json' \
30+
--data '{"name":"Functions"}'
31+
32+
*/

internal/functions/new/templates/index.ts renamed to internal/functions/new/templates/index_auth_mode_user.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,27 @@
44

55
// Setup type definitions for built-in Supabase Runtime APIs
66
import "@supabase/functions-js/edge-runtime.d.ts"
7+
import { withSupabase } from "@supabase/server"
78

89
console.log("Hello from Functions!")
910

10-
Deno.serve(async (req) => {
11-
const { name } = await req.json()
12-
const data = {
13-
message: `Hello ${name}!`,
14-
}
11+
// This endpoint uses 'user' access, credentials is required.
12+
export default {
13+
fetch: withSupabase({ auth: "user" }, async (_req, ctx) => {
14+
const email = ctx.userClaims?.email;
1515

16-
return new Response(
17-
JSON.stringify(data),
18-
{ headers: { "Content-Type": "application/json" } },
19-
)
20-
})
16+
return Response.json({
17+
message: `Hello ${email}!`,
18+
})
19+
}),
20+
}
2121

2222
/* To invoke locally:
2323
2424
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
2525
2. Make an HTTP request:
2626
2727
curl -i --location --request POST '{{ .URL }}' \
28-
--header 'Authorization: Bearer {{ .Token }}' \
29-
--header 'Content-Type: application/json' \
30-
--data '{"name":"Functions"}'
31-
28+
--header 'apiKey: {{ .PublishableKey }}' \
29+
--header 'Authorization: Bearer <UserToken>'
3230
*/

0 commit comments

Comments
 (0)