Skip to content

Commit

Permalink
feat: inject DB verb resources
Browse files Browse the repository at this point in the history
  • Loading branch information
worstell committed Oct 31, 2024
1 parent 5d57552 commit cbf095b
Show file tree
Hide file tree
Showing 41 changed files with 941 additions and 264 deletions.
27 changes: 27 additions & 0 deletions backend/controller/cronjobs/testdata/go/cron/types.ftl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions backend/controller/sql/testdata/go/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import (
"github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK.
)

var db = ftl.PostgresDatabase("testdb")
type MyDbConfig struct {
ftl.DefaultPostgresDatabaseConfig
}

func (MyDbConfig) Name() string { return "testdb" }

type InsertRequest struct {
Data string
Expand All @@ -15,16 +19,16 @@ type InsertRequest struct {
type InsertResponse struct{}

//ftl:verb
func Insert(ctx context.Context, req InsertRequest) (InsertResponse, error) {
err := persistRequest(ctx, req)
func Insert(ctx context.Context, req InsertRequest, db ftl.DatabaseHandle[MyDbConfig]) (InsertResponse, error) {
err := persistRequest(ctx, req, db)
if err != nil {
return InsertResponse{}, err
}

return InsertResponse{}, nil
}

func persistRequest(ctx context.Context, req InsertRequest) error {
func persistRequest(ctx context.Context, req InsertRequest, db ftl.DatabaseHandle[MyDbConfig]) error {
_, err := db.Get(ctx).Exec(`CREATE TABLE IF NOT EXISTS requests
(
data TEXT,
Expand Down
22 changes: 16 additions & 6 deletions backend/controller/sql/testdata/go/database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,26 @@ import (

func TestDatabase(t *testing.T) {
ctx := ftltest.Context(
ftltest.WithCallsAllowedWithinModule(),
ftltest.WithProjectFile("ftl-project.toml"),
ftltest.WithDatabase(db),
ftltest.WithDatabase[MyDbConfig](),
)

Insert(ctx, InsertRequest{Data: "unit test 1"})
_, err := ftltest.Call[InsertClient, InsertRequest, InsertResponse](ctx, InsertRequest{Data: "unit test 1"})
assert.NoError(t, err)
list, err := getAll(ctx)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
assert.Equal(t, "unit test 1", list[0])

ctx = ftltest.Context(
ftltest.WithCallsAllowedWithinModule(),
ftltest.WithProjectFile("ftl-project.toml"),
ftltest.WithDatabase(db),
ftltest.WithDatabase[MyDbConfig](),
)

Insert(ctx, InsertRequest{Data: "unit test 2"})
_, err = ftltest.Call[InsertClient, InsertRequest, InsertResponse](ctx, InsertRequest{Data: "unit test 2"})
assert.NoError(t, err)
list, err = getAll(ctx)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
Expand All @@ -35,18 +39,24 @@ func TestDatabase(t *testing.T) {

func TestOptionOrdering(t *testing.T) {
ctx := ftltest.Context(
ftltest.WithDatabase(db), // <--- consumes DSNs
ftltest.WithCallsAllowedWithinModule(),
ftltest.WithDatabase[MyDbConfig](), // <--- consumes DSNs
ftltest.WithProjectFile("ftl-project.toml"), // <--- provides DSNs
)

Insert(ctx, InsertRequest{Data: "unit test 1"})
_, err := ftltest.Call[InsertClient, InsertRequest, InsertResponse](ctx, InsertRequest{Data: "unit test 1"})
assert.NoError(t, err)
list, err := getAll(ctx)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
assert.Equal(t, "unit test 1", list[0])
}

func getAll(ctx context.Context) ([]string, error) {
db, err := ftltest.GetDatabaseHandle[MyDbConfig]()
if err != nil {
return nil, err
}
rows, err := db.Get(ctx).Query("SELECT data FROM requests ORDER BY created_at;")
if err != nil {
return nil, err
Expand Down
20 changes: 20 additions & 0 deletions backend/controller/sql/testdata/go/database/types.ftl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions backend/provisioner/testdata/go/echo/echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ import (
"github.com/TBD54566975/ftl/go-runtime/ftl"
)

var db = ftl.PostgresDatabase("echodb")
type EchoDBConfig struct {
ftl.DefaultPostgresDatabaseConfig
}

func (EchoDBConfig) Name() string { return "echodb" }

// Echo returns a greeting with the current time.
//
//ftl:verb export
func Echo(ctx context.Context, req string) (string, error) {
func Echo(ctx context.Context, req string, db ftl.DatabaseHandle[EchoDBConfig]) (string, error) {
_, err := db.Get(ctx).Exec(`CREATE TABLE IF NOT EXISTS messages(
message TEXT
);`)
Expand Down
9 changes: 6 additions & 3 deletions docs/content/docs/reference/unittests.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,22 @@ ctx := ftltest.Context(
```

### Databases
By default, calling `Get(ctx)` on a database panics.

To enable database access in a test, you must first [provide a DSN via a project file](#project-files-configs-and-secrets). You can then set up a test database:
```go
ctx := ftltest.Context(
ftltest.WithDefaultProjectFile(),
ftltest.WithDatabase(db),
ftltest.WithDatabase[MyDBConfig](),
)
```
This will:
- Take the provided DSN and appends `_test` to the database name. Eg: `accounts` becomes `accounts_test`
- Wipe all tables in the database so each test run happens on a clean database

You can access the database in your test using its handle:
```go
db, err := ftltest.GetDatabaseHandle[MyDBConfig]()
db.Get(ctx).Exec(...)
```

### Maps
By default, calling `Get(ctx)` on a map handle will panic.
Expand Down
11 changes: 11 additions & 0 deletions go-runtime/compile/build-template/.ftl.tmpl/go/main/main.go.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{- $verbs := .Verbs -}}
{{- $dbs := .Databases -}}
{{- $name := .Name -}}
{{- with .MainCtx -}}

Expand All @@ -24,6 +25,11 @@ func init() {
{{- range .ExternalTypes}}
reflection.ExternalType(*new({{.TypeName}})),
{{- end}}
{{- range $dbs}}
{{- if eq .Type "postgres" }}
reflection.Database[{{.TypeName}}]("{{.Name}}", server.InitPostgres),
{{- end }}
{{- end}}
{{- range $verbs}}
reflection.ProvideResourcesForVerb(
{{.TypeName}},
Expand All @@ -38,6 +44,11 @@ func init() {
{{- else }}
server.VerbClient[{{.TypeName}}, {{.Request.TypeName}}, {{.Response.TypeName}}](),
{{- end -}}
{{- end }}
{{- with getDatabaseHandle . }}
{{- if eq .Type "postgres" }}
server.PostgresDatabaseHandle[{{.TypeName}}](),
{{- end }}
{{- end }}
{{- end}}
),
Expand Down
11 changes: 11 additions & 0 deletions go-runtime/compile/build-template/types.ftl.go.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{- $verbs := .Verbs -}}
{{- $dbs := .Databases -}}
{{- $name := .Name -}}
{{- with .TypesCtx -}}
{{- $moduleName := .MainModulePkg -}}
Expand Down Expand Up @@ -42,6 +43,11 @@ func init() {
{{- range .ExternalTypes}}
reflection.ExternalType(*new({{.TypeName}})),
{{- end}}
{{- range $dbs}}
{{- if eq .Type "postgres" }}
reflection.Database[{{ trimModuleQualifier $moduleName .TypeName }}]("{{.Name}}", server.InitPostgres),
{{- end }}
{{- end}}
{{- range $verbs}}
reflection.ProvideResourcesForVerb(
{{ trimModuleQualifier $moduleName .TypeName }},
Expand All @@ -58,6 +64,11 @@ func init() {
{{- else }}
server.VerbClient[{{$verb}}, {{.Request.LocalTypeName}}, {{.Response.LocalTypeName}}](),
{{- end }}
{{- end }}
{{- with getDatabaseHandle . }}
{{- if eq .Type "postgres" }}
server.PostgresDatabaseHandle[{{ trimModuleQualifier $moduleName .TypeName }}](),
{{- end }}
{{- end }}
{{- end}}
),
Expand Down
74 changes: 74 additions & 0 deletions go-runtime/compile/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type mainModuleContext struct {
Name string
SharedModulesPaths []string
Verbs []goVerb
Databases []goDBHandle
Replacements []*modfile.Replace
MainCtx mainFileContext
TypesCtx typesFileContext
Expand Down Expand Up @@ -87,6 +88,9 @@ func (c *mainModuleContext) generateTypesImports(mainModuleImport string) []stri
if len(c.Verbs) > 0 {
imports.Add(`"context"`)
}
if len(c.Databases) > 0 {
imports.Add(`"github.com/TBD54566975/ftl/go-runtime/server"`)
}
for _, st := range c.TypesCtx.SumTypes {
imports.Add(st.importStatement())
for _, v := range st.Variants {
Expand Down Expand Up @@ -237,6 +241,20 @@ type verbClient struct {

func (v verbClient) resource() {}

type goDBHandle struct {
Type string
Name string
Module string

nativeType
}

func (d goDBHandle) resource() {}

func (d goDBHandle) getNativeType() nativeType {
return d.nativeType
}

type ModifyFilesTransaction interface {
Begin() error
ModifiedFiles(paths ...string) error
Expand Down Expand Up @@ -414,6 +432,7 @@ func (b *mainModuleContextBuilder) build(goModVersion, ftlVersion, projectName s
SharedModulesPaths: sharedModulesPaths,
Replacements: replacements,
Verbs: make([]goVerb, 0, len(b.mainModule.Decls)),
Databases: make([]goDBHandle, 0, len(b.mainModule.Decls)),
MainCtx: mainFileContext{
ProjectName: projectName,
SumTypes: []goSumType{},
Expand Down Expand Up @@ -496,6 +515,8 @@ func (b *mainModuleContextBuilder) visit(
case goExternalType:
ctx.TypesCtx.ExternalTypes = append(ctx.TypesCtx.ExternalTypes, n)
ctx.MainCtx.ExternalTypes = append(ctx.MainCtx.ExternalTypes, n)
case goDBHandle:
ctx.Databases = append(ctx.Databases, n)
}
return next()
})
Expand Down Expand Up @@ -533,6 +554,15 @@ func (b *mainModuleContextBuilder) getGoType(module *schema.Module, node schema.
return optional.None[goType](), isLocal, nil
}
return b.processExternalTypeAlias(n), isLocal, nil
case *schema.Database:
if !isLocal {
return optional.None[goType](), false, nil
}
dbHandle, err := b.processDatabase(module.Name, n)
if err != nil {
return optional.None[goType](), isLocal, err
}
return optional.Some[goType](dbHandle), isLocal, nil

default:
}
Expand Down Expand Up @@ -631,6 +661,26 @@ func (b *mainModuleContextBuilder) processVerb(verb *schema.Verb) (goVerb, error
calleeverb,
})
}
case *schema.MetadataDatabases:
for _, call := range md.Calls {
resolved, ok := b.sch.Resolve(call).Get()
if !ok {
return goVerb{}, fmt.Errorf("failed to resolve %s database, used by %s.%s", call,
b.mainModule.Name, verb.Name)
}
db, ok := resolved.(*schema.Database)
if !ok {
return goVerb{}, fmt.Errorf("%s.%s uses %s database handle, but %s is not a database",
b.mainModule.Name, verb.Name, call, call)
}

dbHandle, err := b.processDatabase(call.Module, db)
if err != nil {
return goVerb{}, err
}
resources = append(resources, dbHandle)
}

default:
// TODO: implement other resources
}
Expand All @@ -643,6 +693,24 @@ func (b *mainModuleContextBuilder) processVerb(verb *schema.Verb) (goVerb, error
return b.getGoVerb(nativeName, verb, resources...)
}

func (b *mainModuleContextBuilder) processDatabase(moduleName string, db *schema.Database) (goDBHandle, error) {
nn, ok := b.nativeNames[db]
if !ok {
return goDBHandle{}, fmt.Errorf("missing native name for database %s.%s", moduleName, db.Name)
}

nt, err := b.getNativeType(nn)
if err != nil {
return goDBHandle{}, err
}
return goDBHandle{
Name: db.Name,
Module: moduleName,
Type: db.Type,
nativeType: nt,
}, nil
}

func (b *mainModuleContextBuilder) getGoVerb(nativeName string, verb *schema.Verb, resources ...verbResource) (goVerb, error) {
nt, err := b.getNativeType(nativeName)
if err != nil {
Expand Down Expand Up @@ -828,6 +896,12 @@ var scaffoldFuncs = scaffolder.FuncMap{
}
return nil
},
"getDatabaseHandle": func(resource verbResource) *goDBHandle {
if c, ok := resource.(goDBHandle); ok {
return &c
}
return nil
},
}

// returns the import path and the directory name for a type alias if there is an associated go library
Expand Down
4 changes: 2 additions & 2 deletions go-runtime/compile/compile_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestNonExportedDecls(t *testing.T) {
in.CopyModule("notexportedverb"),
in.ExpectError(
in.ExecWithOutput("ftl", []string{"deploy", "notexportedverb"}, func(_ string) {}),
`unsupported verb parameter type &{"echo" "EchoClient"}; verbs must have the signature func(Context, Request?, Resources...)`,
`unsupported verb parameter type; verbs must have the signature func(Context, Request?, Resources...)`,
),
)
}
Expand All @@ -34,7 +34,7 @@ func TestUndefinedExportedDecls(t *testing.T) {
in.CopyModule("undefinedverb"),
in.ExpectError(
in.ExecWithOutput("ftl", []string{"deploy", "undefinedverb"}, func(_ string) {}),
`unsupported verb parameter type &{"echo" "UndefinedClient"}; verbs must have the signature func(Context, Request?, Resources...)`,
`unsupported verb parameter type; verbs must have the signature func(Context, Request?, Resources...)`,
),
)
}
Expand Down
Loading

0 comments on commit cbf095b

Please sign in to comment.