snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
26
cmd/ent/ent.go
Normal file
26
cmd/ent/ent.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"entgo.io/ent/cmd/internal/base"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
cmd := &cobra.Command{Use: "ent"}
|
||||
cmd.AddCommand(
|
||||
base.NewCmd(),
|
||||
base.DescribeCmd(),
|
||||
base.GenerateCmd(),
|
||||
base.InitCmd(),
|
||||
base.SchemaCmd(),
|
||||
)
|
||||
cobra.CheckErr(cmd.Execute())
|
||||
}
|
||||
39
cmd/ent/ent_test.go
Normal file
39
cmd/ent/ent_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
defer os.RemoveAll("ent")
|
||||
cmd := exec.Command("go", "run", "entgo.io/ent/cmd/ent", "new", "User")
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderr
|
||||
require.NoError(t, cmd.Run())
|
||||
require.Zero(t, stderr.String())
|
||||
cmd = exec.Command("go", "run", "entgo.io/ent/cmd/ent", "new", "User")
|
||||
require.Error(t, cmd.Run())
|
||||
|
||||
_, err := os.Stat("ent/generate.go")
|
||||
require.NoError(t, err)
|
||||
_, err = os.Stat("ent/schema/user.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd = exec.Command("go", "run", "entgo.io/ent/cmd/ent", "generate", "./ent/schema")
|
||||
stderr = bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderr
|
||||
require.NoError(t, cmd.Run())
|
||||
require.Zero(t, stderr.String())
|
||||
|
||||
_, err = os.Stat("ent/user.go")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
41
cmd/entc/entc.go
Normal file
41
cmd/entc/entc.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"entgo.io/ent/cmd/internal/base"
|
||||
"entgo.io/ent/entc/gen"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
cmd := &cobra.Command{Use: "entc"}
|
||||
cmd.AddCommand(
|
||||
base.NewCmd(),
|
||||
base.DescribeCmd(),
|
||||
base.GenerateCmd(migrate),
|
||||
base.InitCmd(),
|
||||
)
|
||||
_ = cmd.Execute()
|
||||
}
|
||||
|
||||
func migrate(c *gen.Config) {
|
||||
var (
|
||||
target = filepath.Join(c.Target, "generate.go")
|
||||
oldCmd = []byte("entgo.io/ent/cmd/entc")
|
||||
)
|
||||
buf, err := os.ReadFile(target)
|
||||
if err != nil || !bytes.Contains(buf, oldCmd) {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(target, bytes.ReplaceAll(buf, oldCmd, []byte("entgo.io/ent/cmd/ent")), 0644)
|
||||
}
|
||||
35
cmd/entc/entc_test.go
Normal file
35
cmd/entc/entc_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
defer os.RemoveAll("ent")
|
||||
cmd := exec.Command("go", "run", "entgo.io/ent/cmd/entc", "new", "User")
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderr
|
||||
require.NoError(t, cmd.Run(), stderr.String())
|
||||
|
||||
_, err := os.Stat("ent/generate.go")
|
||||
require.NoError(t, err)
|
||||
_, err = os.Stat("ent/schema/user.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd = exec.Command("go", "run", "entgo.io/ent/cmd/entc", "generate", "./ent/schema")
|
||||
stderr = bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderr
|
||||
require.NoError(t, cmd.Run(), stderr.String())
|
||||
|
||||
_, err = os.Stat("ent/user.go")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
107
cmd/entfix/entfix.go
Normal file
107
cmd/entfix/entfix.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/schema"
|
||||
"entgo.io/ent/entc/gen"
|
||||
"github.com/alecthomas/kong"
|
||||
)
|
||||
|
||||
type (
|
||||
// App configures the entfix CLI.
|
||||
App struct {
|
||||
GlobalID GlobalID `cmd:"" name:"globalid" help:"Migrate unique global id ent_types to ent global feature"`
|
||||
}
|
||||
// GlobalID represents the 'entfix globalid' command.
|
||||
GlobalID struct {
|
||||
Dialect string `name:"dialect" help:"Database dialect" required:"" enum:"mysql,postgres,sqlite3"`
|
||||
DSN string `name:"dsn" help:"Data source name" required:""`
|
||||
Path string `name:"path" help:"Path to the generated ent code" required:""`
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Ensure to stop execution on Interrupt signal.
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer stop()
|
||||
app := kong.Parse(
|
||||
new(App),
|
||||
kong.BindTo(ctx, (*context.Context)(nil)),
|
||||
kong.UsageOnError(),
|
||||
)
|
||||
app.FatalIfErrorf(func() error {
|
||||
err := app.Run()
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}())
|
||||
}
|
||||
|
||||
func (cmd *GlobalID) Run(ctx context.Context) error {
|
||||
fmt.Print(`IMPORTANT INFORMATION
|
||||
|
||||
'entfix globalid' will convert the allocated id ranges for your nodes from the
|
||||
database stored 'ent_types' table to the new static configuration on the ent
|
||||
schema itself.
|
||||
|
||||
Please note, that the 'ent_types' table might differ between different environments
|
||||
where your app is deployed. This is especially true if you are using
|
||||
auto-migration instead of versioned migrations.
|
||||
|
||||
Please check, that all 'ent_types' tables for all deployments are equal!
|
||||
|
||||
Only 'yes' will be accepted to approve.
|
||||
|
||||
Enter a value: `)
|
||||
switch c, err := bufio.NewReader(os.Stdin).ReadString('\n'); {
|
||||
case err != nil:
|
||||
return err
|
||||
case strings.TrimSpace(c) != "yes":
|
||||
fmt.Println("\nAborted.")
|
||||
return nil
|
||||
}
|
||||
db, err := sql.Open(cmd.Dialect, cmd.DSN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows := &sql.Rows{}
|
||||
query, args := sql.Dialect(cmd.Dialect).
|
||||
Select("type").
|
||||
From(sql.Table(schema.TypeTable)).
|
||||
OrderBy(sql.Asc("id")).
|
||||
Query()
|
||||
if err := db.Query(ctx, query, args, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
var ts []string
|
||||
if err := sql.ScanSlice(rows, &ts); err != nil {
|
||||
return err
|
||||
}
|
||||
is := make(gen.IncrementStarts, len(ts))
|
||||
for i, t := range ts {
|
||||
is[t] = int64(i << 32)
|
||||
}
|
||||
if err := is.WriteToDisk(cmd.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("\nSuccess! Please run code generation to complete the process.")
|
||||
return nil
|
||||
}
|
||||
89
cmd/entfix/entfix_test.go
Normal file
89
cmd/entfix/entfix_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
entsql "entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/schema"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func TestEntfix(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
stdout, err := os.CreateTemp(t.TempDir(), "")
|
||||
require.NoError(t, err)
|
||||
oldStdout := os.Stdout
|
||||
os.Stdout = stdout
|
||||
t.Cleanup(func() {
|
||||
os.Stdout = oldStdout
|
||||
require.NoError(t, stdout.Close())
|
||||
})
|
||||
|
||||
cli := &GlobalID{
|
||||
Dialect: dialect.SQLite,
|
||||
DSN: fmt.Sprintf("file:%s?mode=memory&cache=shared&_fk=1", t.Name()),
|
||||
Path: t.TempDir(),
|
||||
}
|
||||
|
||||
// Prints details, requires confirmation.
|
||||
os.Stdin = mockStdin(t, "no\n")
|
||||
require.NoError(t, cli.Run(ctx))
|
||||
buf, err := os.ReadFile(stdout.Name())
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasPrefix(string(buf), "IMPORTANT INFORMATION\n\n"))
|
||||
require.True(t, strings.HasSuffix(string(buf), "Aborted.\n"))
|
||||
f, err := os.Open(cli.Path)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||
_, err = f.Readdirnames(1)
|
||||
require.ErrorIs(t, err, io.EOF)
|
||||
|
||||
// If approved, converts the 'ent_types' table.
|
||||
db, err := sql.Open(cli.Dialect, cli.DSN)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, db.Close()) })
|
||||
drv := entsql.OpenDB(dialect.SQLite, db)
|
||||
m, err := schema.NewMigrate(drv, schema.WithGlobalUniqueID(true))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, m.Create(ctx))
|
||||
_, err = db.ExecContext(ctx, fmt.Sprintf("INSERT INTO %s (type) VALUES ('z'), ('y'), ('a'), ('b')", schema.TypeTable))
|
||||
require.NoError(t, err)
|
||||
|
||||
os.Stdin = mockStdin(t, "yes\n")
|
||||
require.NoError(t, cli.Run(ctx))
|
||||
buf, err = os.ReadFile(stdout.Name())
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(string(buf), "Success! Please run code generation to complete the process.\n"))
|
||||
c, err := os.ReadFile(filepath.Join(cli.Path, "internal", "globalid.go"))
|
||||
require.NoError(t, err)
|
||||
require.Contains(t,
|
||||
string(c),
|
||||
fmt.Sprintf(`const IncrementStarts = "{\"a\":%d,\"b\":%d,\"y\":%d,\"z\":%d}"`, 2<<32, 3<<32, 1<<32, 0),
|
||||
)
|
||||
}
|
||||
|
||||
func mockStdin(t *testing.T, content string) *os.File {
|
||||
t.Helper()
|
||||
stdin, err := os.CreateTemp(t.TempDir(), "")
|
||||
require.NoError(t, err)
|
||||
_, err = stdin.WriteString(content)
|
||||
require.NoError(t, err)
|
||||
_, err = stdin.Seek(0, 0)
|
||||
require.NoError(t, err)
|
||||
oldStdin := os.Stdin
|
||||
t.Cleanup(func() {
|
||||
os.Stdin = oldStdin
|
||||
require.NoError(t, stdin.Close())
|
||||
})
|
||||
return stdin
|
||||
}
|
||||
32
cmd/entfix/go.mod
Normal file
32
cmd/entfix/go.mod
Normal file
@@ -0,0 +1,32 @@
|
||||
module entgo.io/ent/cmd/entfix
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.2-0.20250119141711-a0182c96ebb6
|
||||
github.com/alecthomas/kong v1.6.1
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/stretchr/testify v1.8.2
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.27.1-0.20240912191503-92195304dbe1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.23.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
67
cmd/entfix/go.sum
Normal file
67
cmd/entfix/go.sum
Normal file
@@ -0,0 +1,67 @@
|
||||
ariga.io/atlas v0.27.1-0.20240912191503-92195304dbe1 h1:tqUmxmE7r2qqWmgczyNZNOyhzXLqmdUpRhVSuvCUqTU=
|
||||
ariga.io/atlas v0.27.1-0.20240912191503-92195304dbe1/go.mod h1:cpxrs2J9HTrjMrfNKFmUpONY9L0vzAHZ1pY++nTtVw0=
|
||||
entgo.io/ent v0.14.2-0.20250119141711-a0182c96ebb6 h1:7H+NesJeDdcNtZxp0s5Ip/IYLkuUfoRFTIAC0fGcezg=
|
||||
entgo.io/ent v0.14.2-0.20250119141711-a0182c96ebb6/go.mod h1:nPwpHTcyAjBMt+hT6q7Y/SdF41e6IJudQPbTj1P5nYY=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8=
|
||||
github.com/alecthomas/kong v1.6.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
|
||||
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
|
||||
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
360
cmd/internal/base/base.go
Normal file
360
cmd/internal/base/base.go
Normal file
@@ -0,0 +1,360 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
// Package base defines shared basic pieces of the ent command.
|
||||
package base
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
"entgo.io/ent/cmd/internal/printer"
|
||||
"entgo.io/ent/dialect/sql/schema"
|
||||
"entgo.io/ent/entc"
|
||||
"entgo.io/ent/entc/gen"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// IDType is a custom ID implementation for pflag.
|
||||
type IDType field.Type
|
||||
|
||||
// Set implements the Set method of the flag.Value interface.
|
||||
func (t *IDType) Set(s string) error {
|
||||
switch s {
|
||||
case field.TypeInt.String():
|
||||
*t = IDType(field.TypeInt)
|
||||
case field.TypeInt64.String():
|
||||
*t = IDType(field.TypeInt64)
|
||||
case field.TypeUint.String():
|
||||
*t = IDType(field.TypeUint)
|
||||
case field.TypeUint64.String():
|
||||
*t = IDType(field.TypeUint64)
|
||||
case field.TypeString.String():
|
||||
*t = IDType(field.TypeString)
|
||||
default:
|
||||
return fmt.Errorf("invalid type %q", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type representation of the id option for help command.
|
||||
func (IDType) Type() string {
|
||||
return fmt.Sprintf("%v", []field.Type{
|
||||
field.TypeInt,
|
||||
field.TypeInt64,
|
||||
field.TypeUint,
|
||||
field.TypeUint64,
|
||||
field.TypeString,
|
||||
})
|
||||
}
|
||||
|
||||
// String returns the default value for the help command.
|
||||
func (IDType) String() string {
|
||||
return field.TypeInt.String()
|
||||
}
|
||||
|
||||
// InitCmd returns the init command for ent/c packages.
|
||||
func InitCmd() *cobra.Command {
|
||||
c := NewCmd()
|
||||
c.Use = "init [flags] [schemas]"
|
||||
c.Short = "initialize an environment with zero or more schemas"
|
||||
c.Example = examples(
|
||||
"ent init Example",
|
||||
"ent init --target entv1/schema User Group",
|
||||
"ent init --template ./path/to/file.tmpl User",
|
||||
)
|
||||
c.Deprecated = `use "ent new" instead`
|
||||
return c
|
||||
}
|
||||
|
||||
// NewCmd returns the new command for ent/c packages.
|
||||
func NewCmd() *cobra.Command {
|
||||
var target, tmplPath string
|
||||
cmd := &cobra.Command{
|
||||
Use: "new [flags] [schemas]",
|
||||
Short: "initialize a new environment with zero or more schemas",
|
||||
Example: examples(
|
||||
"ent new Example",
|
||||
"ent new --target entv1/schema User Group",
|
||||
"ent new --template ./path/to/file.tmpl User",
|
||||
),
|
||||
Args: func(_ *cobra.Command, names []string) error {
|
||||
for _, name := range names {
|
||||
if !unicode.IsUpper(rune(name[0])) {
|
||||
return errors.New("schema names must begin with uppercase")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, names []string) {
|
||||
var (
|
||||
err error
|
||||
tmpl *template.Template
|
||||
)
|
||||
if tmplPath != "" {
|
||||
tmpl = template.New(filepath.Base(tmplPath)).Funcs(gen.Funcs)
|
||||
tmpl, err = tmpl.ParseFiles(tmplPath)
|
||||
} else {
|
||||
tmpl = template.New("schema").Funcs(gen.Funcs)
|
||||
tmpl, err = tmpl.Parse(defaultTemplate)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalln(fmt.Errorf("ent/new: could not parse template %w", err))
|
||||
}
|
||||
if err := newEnv(target, names, tmpl); err != nil {
|
||||
log.Fatalln(fmt.Errorf("ent/new: %w", err))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&target, "target", defaultSchema, "target directory for schemas")
|
||||
cmd.Flags().StringVar(&tmplPath, "template", "", "template to use for new schemas")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// DescribeCmd returns the describe command for ent/c packages.
|
||||
func DescribeCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "describe [flags] path",
|
||||
Short: "print a description of the graph schema",
|
||||
Example: examples(
|
||||
"ent describe ./ent/schema",
|
||||
"ent describe github.com/a8m/x",
|
||||
),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, path []string) {
|
||||
graph, err := entc.LoadGraph(path[0], &gen.Config{})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
printer.Fprint(os.Stdout, graph)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateCmd returns the generate command for ent/c packages.
|
||||
func GenerateCmd(postRun ...func(*gen.Config)) *cobra.Command {
|
||||
var (
|
||||
cfg gen.Config
|
||||
storage string
|
||||
features []string
|
||||
templates []string
|
||||
idtype = IDType(field.TypeInt)
|
||||
cmd = &cobra.Command{
|
||||
Use: "generate [flags] path",
|
||||
Short: "generate go code for the schema directory",
|
||||
Example: examples(
|
||||
"ent generate ./ent/schema",
|
||||
"ent generate github.com/a8m/x",
|
||||
),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, path []string) {
|
||||
opts := []entc.Option{
|
||||
entc.Storage(storage),
|
||||
entc.FeatureNames(features...),
|
||||
}
|
||||
for _, tmpl := range templates {
|
||||
typ := "dir"
|
||||
if parts := strings.SplitN(tmpl, "=", 2); len(parts) > 1 {
|
||||
typ, tmpl = parts[0], parts[1]
|
||||
}
|
||||
switch typ {
|
||||
case "dir":
|
||||
opts = append(opts, entc.TemplateDir(tmpl))
|
||||
case "file":
|
||||
opts = append(opts, entc.TemplateFiles(tmpl))
|
||||
case "glob":
|
||||
opts = append(opts, entc.TemplateGlob(tmpl))
|
||||
default:
|
||||
log.Fatalln("unsupported template type", typ)
|
||||
}
|
||||
}
|
||||
// If the target directory is not inferred from
|
||||
// the schema path, resolve its package path.
|
||||
if cfg.Target != "" {
|
||||
pkgPath, err := PkgPath(DefaultConfig, cfg.Target)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
cfg.Package = pkgPath
|
||||
}
|
||||
cfg.IDType = &field.TypeInfo{Type: field.Type(idtype)}
|
||||
if err := entc.Generate(path[0], &cfg, opts...); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
for _, fn := range postRun {
|
||||
fn(&cfg)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
cmd.Flags().Var(&idtype, "idtype", "type of the id field")
|
||||
cmd.Flags().StringVar(&storage, "storage", "sql", "storage driver to support in codegen")
|
||||
cmd.Flags().StringVar(&cfg.Header, "header", "", "override codegen header")
|
||||
cmd.Flags().StringVar(&cfg.Target, "target", "", "target directory for codegen")
|
||||
cmd.Flags().StringSliceVarP(&features, "feature", "", nil, "extend codegen with additional features")
|
||||
cmd.Flags().StringSliceVarP(&templates, "template", "", nil, "external templates to execute")
|
||||
// The --idtype flag predates the field.<Type>("id") option.
|
||||
// See, https://entgo.io/docs/schema-fields#id-field.
|
||||
cobra.CheckErr(cmd.Flags().MarkHidden("idtype"))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SchemaCmd returns DDL to use Ent as an Atlas schema loader.
|
||||
func SchemaCmd() *cobra.Command {
|
||||
var (
|
||||
cfg gen.Config
|
||||
dlct, version string
|
||||
features, buildTags []string
|
||||
hashSymbols bool
|
||||
cmd = &cobra.Command{
|
||||
Use: "schema [flags] path",
|
||||
Short: "dump the DDL for the schema directory",
|
||||
Example: examples(
|
||||
"ent schema ./ent/schema --dialect mysql --version 5.6",
|
||||
"ent schema ./ent/schema --dialect sqlite3",
|
||||
"ent schema github.com/a8m/x --dialect postgres --version 15",
|
||||
),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, path []string) {
|
||||
for _, o := range []entc.Option{
|
||||
entc.FeatureNames(features...),
|
||||
entc.BuildTags(buildTags...),
|
||||
} {
|
||||
if err := o(&cfg); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
// If the target directory is not inferred from
|
||||
// the schema path, resolve its package path.
|
||||
if cfg.Target != "" {
|
||||
pkgPath, err := PkgPath(DefaultConfig, cfg.Target)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
cfg.Package = pkgPath
|
||||
}
|
||||
g, err := entc.LoadGraph(path[0], &cfg)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
t, err := g.Tables()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
v, err := g.Views()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
ddl, err := schema.DDL(cmd.Context(), schema.DDLArgs{
|
||||
Dialect: dlct,
|
||||
Version: version,
|
||||
HashSymbols: hashSymbols,
|
||||
Tables: append(t, v...),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
fmt.Println(ddl)
|
||||
},
|
||||
}
|
||||
)
|
||||
cmd.Flags().StringVar(&dlct, "dialect", "", "database dialect to use")
|
||||
cmd.Flags().StringVar(&version, "version", "", "database version to assume")
|
||||
cmd.Flags().StringSliceVarP(&features, "feature", "", nil, "extend codegen with additional features")
|
||||
cmd.Flags().StringSliceVarP(&buildTags, "build-tags", "", nil, "go build tags to use when loading the schema graph")
|
||||
cmd.Flags().BoolVar(&hashSymbols, "hash-symbols", false, "whether to hash long symbols")
|
||||
cobra.CheckErr(cmd.MarkFlagRequired("dialect"))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// newEnv create a new environment for ent codegen.
|
||||
func newEnv(target string, names []string, tmpl *template.Template) error {
|
||||
if err := createDir(target); err != nil {
|
||||
return fmt.Errorf("create dir %s: %w", target, err)
|
||||
}
|
||||
for _, name := range names {
|
||||
if err := gen.ValidSchemaName(name); err != nil {
|
||||
return fmt.Errorf("new schema %s: %w", name, err)
|
||||
}
|
||||
if fileExists(target, name) {
|
||||
return fmt.Errorf("new schema %s: already exists", name)
|
||||
}
|
||||
b := bytes.NewBuffer(nil)
|
||||
if err := tmpl.Execute(b, name); err != nil {
|
||||
return fmt.Errorf("executing template %s: %w", name, err)
|
||||
}
|
||||
newFileTarget := filepath.Join(target, strings.ToLower(name+".go"))
|
||||
if err := os.WriteFile(newFileTarget, b.Bytes(), 0644); err != nil {
|
||||
return fmt.Errorf("writing file %s: %w", newFileTarget, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDir(target string) error {
|
||||
_, err := os.Stat(target)
|
||||
if err == nil || !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(target, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("creating schema directory: %w", err)
|
||||
}
|
||||
if target != defaultSchema {
|
||||
return nil
|
||||
}
|
||||
if err := os.WriteFile("ent/generate.go", []byte(genFile), 0644); err != nil {
|
||||
return fmt.Errorf("creating generate.go file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileExists(target, name string) bool {
|
||||
var _, err = os.Stat(filepath.Join(target, strings.ToLower(name+".go")))
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
const (
|
||||
// default schema package path.
|
||||
defaultSchema = "ent/schema"
|
||||
// ent/generate.go file used for "go generate" command.
|
||||
genFile = "package ent\n\n//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema\n"
|
||||
// schema template for the "init" command.
|
||||
defaultTemplate = `package schema
|
||||
|
||||
import "entgo.io/ent"
|
||||
|
||||
// {{ . }} holds the schema definition for the {{ . }} entity.
|
||||
type {{ . }} struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
// Fields of the {{ . }}.
|
||||
func ({{ . }}) Fields() []ent.Field {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edges of the {{ . }}.
|
||||
func ({{ . }}) Edges() []ent.Edge {
|
||||
return nil
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
// examples formats the given examples to the cli.
|
||||
func examples(ex ...string) string {
|
||||
for i := range ex {
|
||||
ex[i] = " " + ex[i] // indent each row with 2 spaces.
|
||||
}
|
||||
return strings.Join(ex, "\n")
|
||||
}
|
||||
55
cmd/internal/base/packages.go
Normal file
55
cmd/internal/base/packages.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
// DefaultConfig for loading Go base.
|
||||
var DefaultConfig = &packages.Config{Mode: packages.NeedName}
|
||||
|
||||
// PkgPath returns the Go package name for given target path.
|
||||
// Even if the existing path is not exist yet in the filesystem.
|
||||
//
|
||||
// If base.Config is nil, DefaultConfig will be used to load base.
|
||||
func PkgPath(config *packages.Config, target string) (string, error) {
|
||||
if config == nil {
|
||||
config = DefaultConfig
|
||||
}
|
||||
pathCheck, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var parts []string
|
||||
if _, err := os.Stat(pathCheck); os.IsNotExist(err) {
|
||||
parts = append(parts, filepath.Base(pathCheck))
|
||||
pathCheck = filepath.Dir(pathCheck)
|
||||
}
|
||||
// Try maximum 2 directories above the given
|
||||
// target to find the root package or module.
|
||||
for i := 0; i < 2; i++ {
|
||||
pkgs, err := packages.Load(config, pathCheck)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("load package info: %w", err)
|
||||
}
|
||||
if len(pkgs) == 0 || len(pkgs[0].Errors) != 0 {
|
||||
parts = append(parts, filepath.Base(pathCheck))
|
||||
pathCheck = filepath.Dir(pathCheck)
|
||||
continue
|
||||
}
|
||||
pkgPath := pkgs[0].PkgPath
|
||||
for j := len(parts) - 1; j >= 0; j-- {
|
||||
pkgPath = path.Join(pkgPath, parts[j])
|
||||
}
|
||||
return pkgPath, nil
|
||||
}
|
||||
return "", fmt.Errorf("root package or module was not found for: %s", target)
|
||||
}
|
||||
49
cmd/internal/base/packages_test.go
Normal file
49
cmd/internal/base/packages_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/tools/go/packages/packagestest"
|
||||
)
|
||||
|
||||
func TestPkgPath(t *testing.T) { packagestest.TestAll(t, testPkgPath) }
|
||||
func testPkgPath(t *testing.T, x packagestest.Exporter) {
|
||||
e := packagestest.Export(t, x, []packagestest.Module{
|
||||
{
|
||||
Name: "golang.org/x",
|
||||
Files: map[string]any{
|
||||
"x.go": "package x",
|
||||
"y/y.go": "package y",
|
||||
},
|
||||
},
|
||||
})
|
||||
defer e.Cleanup()
|
||||
|
||||
e.Config.Dir = filepath.Dir(e.File("golang.org/x", "x.go"))
|
||||
target := filepath.Join(e.Config.Dir, "ent")
|
||||
pkgPath, err := PkgPath(e.Config, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "golang.org/x/ent", pkgPath)
|
||||
|
||||
e.Config.Dir = filepath.Dir(e.File("golang.org/x", "y/y.go"))
|
||||
target = filepath.Join(e.Config.Dir, "ent")
|
||||
pkgPath, err = PkgPath(e.Config, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "golang.org/x/y/ent", pkgPath)
|
||||
|
||||
target = filepath.Join(e.Config.Dir, "z/ent")
|
||||
pkgPath, err = PkgPath(e.Config, target)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "golang.org/x/y/z/ent", pkgPath)
|
||||
|
||||
target = filepath.Join(e.Config.Dir, "z/e/n/t")
|
||||
pkgPath, err = PkgPath(e.Config, target)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, pkgPath)
|
||||
}
|
||||
141
cmd/internal/printer/printer.go
Normal file
141
cmd/internal/printer/printer.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"entgo.io/ent/entc/gen"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// A Config controls the output of Fprint.
|
||||
type Config struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// Print prints a table description of the graph to the given writer.
|
||||
func (p Config) Print(g *gen.Graph) {
|
||||
for _, n := range g.Nodes {
|
||||
p.node(n)
|
||||
}
|
||||
}
|
||||
|
||||
// Fprint executes "pretty-printer" on the given writer.
|
||||
func Fprint(w io.Writer, g *gen.Graph) {
|
||||
Config{Writer: w}.Print(g)
|
||||
}
|
||||
|
||||
// node returns description of a type. The format of the description is:
|
||||
//
|
||||
// Type:
|
||||
// <Fields Table>
|
||||
//
|
||||
// <Edges Table>
|
||||
func (p Config) node(t *gen.Type) {
|
||||
var (
|
||||
b strings.Builder
|
||||
id []*gen.Field
|
||||
table = tablewriter.NewWriter(&b)
|
||||
header = []string{"Field", "Type", "Unique", "Optional", "Nillable", "Default", "UpdateDefault", "Immutable", "StructTag", "Validators", "Comment"}
|
||||
)
|
||||
b.WriteString(t.Name + ":\n")
|
||||
table.Options(
|
||||
tablewriter.WithHeaderConfig(tw.CellConfig{
|
||||
Padding: tw.CellPadding{
|
||||
Global: tw.Padding{
|
||||
Left: tw.Space,
|
||||
Right: tw.Space,
|
||||
},
|
||||
},
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoFormat: tw.Off,
|
||||
},
|
||||
}),
|
||||
tablewriter.WithRendition(tw.Rendition{
|
||||
Symbols: tw.NewSymbols(tw.StyleASCII),
|
||||
}),
|
||||
)
|
||||
table.Header(header)
|
||||
var alignment = make([]tw.Align, 0)
|
||||
if t.ID != nil {
|
||||
id = append(id, t.ID)
|
||||
}
|
||||
for _, f := range append(id, t.Fields...) {
|
||||
v := reflect.ValueOf(*f)
|
||||
row := make([]string, len(header))
|
||||
for i := 0; i < len(row)-1; i++ {
|
||||
field := v.FieldByNameFunc(func(name string) bool {
|
||||
// The first field is mapped from "Name" to "Field".
|
||||
return name == "Name" && i == 0 || name == header[i]
|
||||
})
|
||||
row[i] = fmt.Sprint(field.Interface())
|
||||
_, err := strconv.Atoi(row[i])
|
||||
if err == nil {
|
||||
alignment = append(alignment, tw.AlignRight)
|
||||
} else {
|
||||
alignment = append(alignment, tw.AlignLeft)
|
||||
}
|
||||
}
|
||||
row[len(row)-1] = f.Comment()
|
||||
_ = table.Append(row)
|
||||
table.Options(
|
||||
tablewriter.WithRowAlignmentConfig(
|
||||
tw.CellAlignment{PerColumn: alignment},
|
||||
),
|
||||
)
|
||||
}
|
||||
err := table.Render()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Create new table for edges
|
||||
table = tablewriter.NewWriter(&b)
|
||||
table.Options(
|
||||
tablewriter.WithHeaderConfig(tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{AutoFormat: tw.Off},
|
||||
Padding: tw.CellPadding{
|
||||
Global: tw.Padding{
|
||||
Left: tw.Space,
|
||||
Right: tw.Space,
|
||||
},
|
||||
},
|
||||
}),
|
||||
tablewriter.WithRendition(tw.Rendition{
|
||||
Symbols: tw.NewSymbols(tw.StyleASCII),
|
||||
}),
|
||||
)
|
||||
table.Header([]string{"Edge", "Type", "Inverse", "BackRef", "Relation", "Unique", "Optional", "Comment"})
|
||||
hasEdges := false
|
||||
for _, e := range t.Edges {
|
||||
hasEdges = true
|
||||
err := table.Append([]string{
|
||||
e.Name,
|
||||
e.Type.Name,
|
||||
strconv.FormatBool(e.IsInverse()),
|
||||
e.Inverse,
|
||||
e.Rel.Type.String(),
|
||||
strconv.FormatBool(e.Unique),
|
||||
strconv.FormatBool(e.Optional),
|
||||
e.Comment(),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if hasEdges {
|
||||
err := table.Render()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
io.WriteString(p, strings.ReplaceAll(b.String(), "\n", "\n\t")+"\n")
|
||||
}
|
||||
177
cmd/internal/printer/printer_test.go
Normal file
177
cmd/internal/printer/printer_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright 2019-present Facebook Inc. All rights reserved.
|
||||
// This source code is licensed under the Apache 2.0 license found
|
||||
// in the LICENSE file in the root directory of this source tree.
|
||||
|
||||
package printer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"entgo.io/ent/entc/gen"
|
||||
"entgo.io/ent/schema/field"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrinter_Print(t *testing.T) {
|
||||
tests := []struct {
|
||||
input *gen.Graph
|
||||
out string
|
||||
}{
|
||||
{
|
||||
input: &gen.Graph{
|
||||
Nodes: []*gen.Type{
|
||||
{
|
||||
Name: "User",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Fields: []*gen.Field{
|
||||
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}, Validators: 1},
|
||||
{Name: "age", Type: &field.TypeInfo{Type: field.TypeInt}, Nillable: true},
|
||||
{Name: "created_at", Type: &field.TypeInfo{Type: field.TypeTime}, Nillable: true, Immutable: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: `
|
||||
User:
|
||||
+------------+-----------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+------------+-----------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
| name | string | false | false | false | false | false | false | | 1 | |
|
||||
| age | int | false | false | true | false | false | false | | 0 | |
|
||||
| created_at | time.Time | false | false | true | false | false | true | | 0 | |
|
||||
+------------+-----------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
input: &gen.Graph{
|
||||
Nodes: []*gen.Type{
|
||||
{
|
||||
Name: "User",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Edges: []*gen.Edge{
|
||||
{Name: "groups", Type: &gen.Type{Name: "Group"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
|
||||
{Name: "spouse", Type: &gen.Type{Name: "User"}, Unique: true, Rel: gen.Relation{Type: gen.O2O}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: `
|
||||
User:
|
||||
+-------+------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+-------+------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
+-------+------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| groups | Group | false | | M2M | false | true | |
|
||||
| spouse | User | false | | O2O | true | false | |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
input: &gen.Graph{
|
||||
Nodes: []*gen.Type{
|
||||
{
|
||||
Name: "User",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Fields: []*gen.Field{
|
||||
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}, Validators: 1},
|
||||
{Name: "age", Type: &field.TypeInfo{Type: field.TypeInt}, Nillable: true},
|
||||
},
|
||||
Edges: []*gen.Edge{
|
||||
{Name: "groups", Type: &gen.Type{Name: "Group"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
|
||||
{Name: "spouse", Type: &gen.Type{Name: "User"}, Unique: true, Rel: gen.Relation{Type: gen.O2O}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: `
|
||||
User:
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
| name | string | false | false | false | false | false | false | | 1 | |
|
||||
| age | int | false | false | true | false | false | false | | 0 | |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| groups | Group | false | | M2M | false | true | |
|
||||
| spouse | User | false | | O2O | true | false | |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
input: &gen.Graph{
|
||||
Nodes: []*gen.Type{
|
||||
{
|
||||
Name: "User",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Fields: []*gen.Field{
|
||||
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}, Validators: 1},
|
||||
{Name: "age", Type: &field.TypeInfo{Type: field.TypeInt}, Nillable: true},
|
||||
},
|
||||
Edges: []*gen.Edge{
|
||||
{Name: "groups", Type: &gen.Type{Name: "Group"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
|
||||
{Name: "spouse", Type: &gen.Type{Name: "User"}, Unique: true, Rel: gen.Relation{Type: gen.O2O}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Group",
|
||||
ID: &gen.Field{Name: "id", Type: &field.TypeInfo{Type: field.TypeInt}},
|
||||
Fields: []*gen.Field{
|
||||
{Name: "name", Type: &field.TypeInfo{Type: field.TypeString}},
|
||||
},
|
||||
Edges: []*gen.Edge{
|
||||
{Name: "users", Type: &gen.Type{Name: "User"}, Rel: gen.Relation{Type: gen.M2M}, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: `
|
||||
User:
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
| name | string | false | false | false | false | false | false | | 1 | |
|
||||
| age | int | false | false | true | false | false | false | | 0 | |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
| groups | Group | false | | M2M | false | true | |
|
||||
| spouse | User | false | | O2O | true | false | |
|
||||
+--------+-------+---------+---------+----------+--------+----------+---------+
|
||||
|
||||
Group:
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | Comment |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
| id | int | false | false | false | false | false | false | | 0 | |
|
||||
| name | string | false | false | false | false | false | false | | 0 | |
|
||||
+-------+--------+--------+----------+----------+---------+---------------+-----------+-----------+------------+---------+
|
||||
+-------+------+---------+---------+----------+--------+----------+---------+
|
||||
| Edge | Type | Inverse | BackRef | Relation | Unique | Optional | Comment |
|
||||
+-------+------+---------+---------+----------+--------+----------+---------+
|
||||
| users | User | false | | M2M | false | true | |
|
||||
+-------+------+---------+---------+----------+--------+----------+---------+
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
b := &strings.Builder{}
|
||||
Fprint(b, tt.input)
|
||||
assert.Equal(t, tt.out, "\n"+b.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user