Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/ledger/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/formancehq/fctl/v3/cmd/ledger/accounts"
"github.com/formancehq/fctl/v3/cmd/ledger/internal"
"github.com/formancehq/fctl/v3/cmd/ledger/schemas"
"github.com/formancehq/fctl/v3/cmd/ledger/transactions"
"github.com/formancehq/fctl/v3/cmd/ledger/volumes"
fctl "github.com/formancehq/fctl/v3/pkg"
Expand All @@ -25,6 +26,7 @@ func NewCommand() *cobra.Command {
NewDeleteMetadataCommand(),
NewExportCommand(),
NewImportCommand(),
schemas.NewLedgerSchemasCommand(),
transactions.NewLedgerTransactionsCommand(),
accounts.NewLedgerAccountsCommand(),
volumes.NewLedgerVolumesCommand(),
Expand Down
110 changes: 110 additions & 0 deletions cmd/ledger/schemas/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package schemas

import (
"encoding/json"
"fmt"

"github.com/TylerBrock/colorjson"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/formancehq/formance-sdk-go/v4/pkg/models/ledger"
"github.com/formancehq/formance-sdk-go/v4/pkg/models/operations"

internal "github.com/formancehq/fctl/v3/cmd/ledger/internal"
fctl "github.com/formancehq/fctl/v3/pkg"
)

type GetStore struct {
Schema ledger.V2SchemaData `json:"schema"`
}
type GetController struct {
store *GetStore
formatFlag string
}

var _ fctl.Controller[*GetStore] = (*GetController)(nil)

func NewDefaultGetStore() *GetStore {
return &GetStore{}
}

func NewGetController() *GetController {
return &GetController{
store: NewDefaultGetStore(),
formatFlag: "format",
}
}

func NewGetCommand() *cobra.Command {
c := NewGetController()
return fctl.NewCommand("get <version>",
fctl.WithShortDescription("Get a schema for a ledger by version"),
fctl.WithAliases("g", "show"),
fctl.WithStringFlag(c.formatFlag, "json", "Output format of the schema (json, yaml)"),
fctl.WithArgs(cobra.ExactArgs(1)),
fctl.WithValidArgsFunction(cobra.NoFileCompletions),
fctl.WithController[*GetStore](c),
)
}

func (c *GetController) GetStore() *GetStore {
return c.store
}

func (c *GetController) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) {
_, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd)
if err != nil {
return nil, err
}

stackClient, err := fctl.NewStackClientFromFlags(cmd, relyingParty, fctl.NewPTermDialog(), profileName, *profile)
if err != nil {
return nil, err
}

response, err := stackClient.Ledger.V2.GetSchema(cmd.Context(), operations.V2GetSchemaRequest{
Ledger: fctl.GetString(cmd, internal.LedgerFlag),
Version: args[0],
})
if err != nil {
return nil, err
}

c.store.Schema = response.V2SchemaResponse.V2SchemaData
return c, nil
}

func (c *GetController) Render(cmd *cobra.Command, _ []string) error {
out, err := json.Marshal(c.store.Schema)
if err != nil {
return err
}

raw := make(map[string]any)
if err := json.Unmarshal(out, &raw); err != nil {
_, err = cmd.OutOrStdout().Write(out)
return err
}

switch format := fctl.GetString(cmd, c.formatFlag); format {
case "yaml", "yml":
yamlOut, err := yaml.Marshal(raw)
if err != nil {
return err
}
_, err = cmd.OutOrStdout().Write(yamlOut)
return err
case "json":
f := colorjson.NewFormatter()
f.Indent = 2
colorized, err := f.Marshal(raw)
if err != nil {
return err
}
_, err = cmd.OutOrStdout().Write(append(colorized, '\n'))
return err
default:
return fmt.Errorf("unsupported format %q (expected json or yaml)", format)
}
}
150 changes: 150 additions & 0 deletions cmd/ledger/schemas/insert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package schemas

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/pterm/pterm"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/formancehq/formance-sdk-go/v4/pkg/models/ledger"
"github.com/formancehq/formance-sdk-go/v4/pkg/models/operations"

internal "github.com/formancehq/fctl/v3/cmd/ledger/internal"
fctl "github.com/formancehq/fctl/v3/pkg"
)

const schemaFetchTimeout = 30 * time.Second

type InsertStore struct {
Success bool `json:"success"`
}
type InsertController struct {
store *InsertStore
}

var _ fctl.Controller[*InsertStore] = (*InsertController)(nil)

func NewDefaultInsertStore() *InsertStore {
return &InsertStore{}
}

func NewInsertController() *InsertController {
return &InsertController{
store: NewDefaultInsertStore(),
}
}

func NewInsertCommand() *cobra.Command {
return fctl.NewCommand("insert <version> <source>",
fctl.WithShortDescription("Insert a schema for a ledger from a JSON/YAML file or URL"),
fctl.WithAliases("i", "create"),
fctl.WithConfirmFlag(),
fctl.WithArgs(cobra.ExactArgs(2)),
fctl.WithValidArgsFunction(cobra.NoFileCompletions),
fctl.WithController[*InsertStore](NewInsertController()),
)
}

func (c *InsertController) GetStore() *InsertStore {
return c.store
}

func (c *InsertController) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) {
_, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd)
if err != nil {
return nil, err
}

stackClient, err := fctl.NewStackClientFromFlags(cmd, relyingParty, fctl.NewPTermDialog(), profileName, *profile)
if err != nil {
return nil, err
}

ledgerName := fctl.GetString(cmd, internal.LedgerFlag)
version := args[0]

schemaData, err := loadSchemaData(cmd, args[1])
if err != nil {
return nil, err
}

if !fctl.CheckStackApprobation(cmd, "You are about to insert schema version %s on ledger %s", version, ledgerName) {
return nil, fctl.ErrMissingApproval
}

response, err := stackClient.Ledger.V2.InsertSchema(cmd.Context(), operations.V2InsertSchemaRequest{
Ledger: ledgerName,
Version: version,
V2SchemaData: *schemaData,
})
if err != nil {
return nil, err
}

c.store.Success = response.StatusCode == 204
if !c.store.Success {
return nil, fmt.Errorf("unexpected status code %d while inserting schema", response.StatusCode)
}
return c, nil
}

func (c *InsertController) Render(cmd *cobra.Command, _ []string) error {
pterm.Success.WithWriter(cmd.OutOrStdout()).Printfln("Schema inserted!")
return nil
}

func loadSchemaData(cmd *cobra.Command, source string) (*ledger.V2SchemaDataInput, error) {
raw, err := readSource(cmd, source)
if err != nil {
return nil, err
}

var intermediate any
if err := yaml.Unmarshal(raw, &intermediate); err != nil {
return nil, fmt.Errorf("parsing schema: %w", err)
}

normalized, err := json.Marshal(intermediate)
if err != nil {
return nil, err
}

schemaData := &ledger.V2SchemaDataInput{}
if err := json.Unmarshal(normalized, schemaData); err != nil {
return nil, err
}

return schemaData, nil
}

func readSource(cmd *cobra.Command, source string) ([]byte, error) {
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
req, err := http.NewRequestWithContext(cmd.Context(), http.MethodGet, source, nil)
if err != nil {
return nil, err
}
client := fctl.GetHttpClient(cmd)
client.Timeout = schemaFetchTimeout
resp, err := client.Do(req)
if err != nil {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("fetching schema from %s: unexpected status %s", source, resp.Status)
}
return io.ReadAll(resp.Body)
}

return os.ReadFile(filepath.Clean(source))
}
117 changes: 117 additions & 0 deletions cmd/ledger/schemas/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package schemas

import (
"fmt"
"time"

"github.com/pterm/pterm"
"github.com/spf13/cobra"

"github.com/formancehq/formance-sdk-go/v4/pkg/models/ledger"
"github.com/formancehq/formance-sdk-go/v4/pkg/models/operations"

internal "github.com/formancehq/fctl/v3/cmd/ledger/internal"
fctl "github.com/formancehq/fctl/v3/pkg"
)

type ListStore struct {
Schemas []ledger.V2SchemaData `json:"schemas"`
Cursor fctl.Cursor `json:"cursor"`
}
type ListController struct {
store *ListStore
}

var _ fctl.Controller[*ListStore] = (*ListController)(nil)

func NewDefaultListStore() *ListStore {
return &ListStore{
Schemas: []ledger.V2SchemaData{},
}
}

func NewListController() *ListController {
return &ListController{
store: NewDefaultListStore(),
}
}

func NewListCommand() *cobra.Command {
c := NewListController()
return fctl.NewCommand("list",
fctl.WithAliases("ls", "l"),
fctl.WithShortDescription("List all schemas for a ledger"),
fctl.WithCursorFlag(),
fctl.WithPageSizeFlag(),
fctl.WithArgs(cobra.ExactArgs(0)),
fctl.WithValidArgsFunction(cobra.NoFileCompletions),
fctl.WithController[*ListStore](c),
)
}

func (c *ListController) GetStore() *ListStore {
return c.store
}

func (c *ListController) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) {
_, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd)
if err != nil {
return nil, err
}

stackClient, err := fctl.NewStackClientFromFlags(cmd, relyingParty, fctl.NewPTermDialog(), profileName, *profile)
if err != nil {
return nil, err
}

cursor, err := fctl.GetCursor(cmd)
if err != nil {
return nil, err
}
pageSize, err := fctl.GetPageSize(cmd)
if err != nil {
return nil, err
}

req := operations.V2ListSchemasRequest{
Ledger: fctl.GetString(cmd, internal.LedgerFlag),
}
if cursor != "" {
req.Cursor = fctl.Ptr(cursor)
} else {
req.PageSize = fctl.Ptr(int64(pageSize))
}

response, err := stackClient.Ledger.V2.ListSchemas(cmd.Context(), req)
if err != nil {
return nil, err
}

cur := response.V2SchemasCursorResponse.V2SchemasCursor
c.store.Schemas = cur.Data
c.store.Cursor = fctl.Cursor{HasMore: cur.HasMore, PageSize: cur.PageSize, Next: cur.Next, Previous: cur.Previous}
return c, nil
}

func (c *ListController) Render(cmd *cobra.Command, _ []string) error {
tableData := fctl.Map(c.store.Schemas, func(schema ledger.V2SchemaData) []string {
return []string{
schema.Version,
schema.CreatedAt.Format(time.RFC3339),
fmt.Sprintf("%d", len(schema.V2ChartOfAccounts)),
fmt.Sprintf("%d", len(schema.V2QueryTemplates)),
fmt.Sprintf("%d", len(schema.V2TransactionTemplates)),
}
})
tableData = fctl.Prepend(tableData, []string{"Version", "Created at", "Chart", "Queries", "Transactions"})

if err := pterm.DefaultTable.
WithHasHeader().
WithWriter(cmd.OutOrStdout()).
WithData(tableData).
Render(); err != nil {
return err
}

return fctl.RenderCursor(cmd.OutOrStdout(), c.store.Cursor)
}
Loading
Loading