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
20 changes: 20 additions & 0 deletions cli/internal/client/actionbase_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package client

import (
"net/url"
"strconv"

"github.com/kakao/actionbase/internal/client/model"
)
Expand Down Expand Up @@ -121,3 +122,22 @@ func (a *ActionbaseClient) PostRaw(path string, body any) (int, string) {
func (a *ActionbaseClient) GetHost() string {
return a.client.baseUrl
}

// MetastoreDumpPath is served by 0.4.x and was removed on later servers, so a caller has to be
// ready for a 404 rather than assume it is there.
const MetastoreDumpPath = "/graph/v2/metastore/global"

// DumpMetastore reads one page of the metastore, ordered by primary key.
//
// Ordered by id rather than k: it is the primary key, so the server's OFFSET pagination walks an
// index it already has. Sorting by k would order by a base64 string for no benefit here.
func (a *ActionbaseClient) DumpMetastore(page, size int) *Response[model.MetastoreDumpPage] {
u := &url.URL{Path: MetastoreDumpPath}
q := u.Query()
q.Set("page", strconv.Itoa(page))
q.Set("size", strconv.Itoa(size))
q.Set("sort", "id,asc")
u.RawQuery = q.Encode()

return GetStream[model.MetastoreDumpPage](a.client, u.String())
}
46 changes: 46 additions & 0 deletions cli/internal/client/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,52 @@ func Get[T any](c *HTTPClient, uri string) *Response[T] {
return call[T](c, request, nil)
}

// GetStream is Get with two differences that a paged, version-dependent endpoint needs.
//
// It keeps the real status code when the body does not decode. Get replaces it with -1, and an
// unauthorized response carries no body at all, so a caller cannot otherwise tell "this server does
// not serve that path" from "this token may not". And it decodes straight off the connection rather
// than reading the whole body first, so a page is not held twice.
func GetStream[T any](c *HTTPClient, uri string) *Response[T] {
url := fmt.Sprintf("%s%s", c.baseUrl, uri)
request, err := http.NewRequest("GET", url, nil)
if err != nil {
var empty T
return NewResponse[T](-1, &empty, fmt.Errorf("failed to create request: %w", err))
}

request.Header.Set("Content-Type", "application/json")
if c.authKey != nil {
request.Header.Set("Authorization", *c.authKey)
}

if c.context.IsDebugEnabled {
slog.Debug(fmt.Sprintf("→ %s %s", request.Method, request.URL.RequestURI()))
}

response, err := c.client.Do(request)
if err != nil {
var empty T
return NewResponse[T](-1, &empty, fmt.Errorf("failed to execute request: %w", err))
}
defer func(body io.ReadCloser) {
_, _ = io.Copy(io.Discard, body)
_ = body.Close()
}(response.Body)

if c.context.IsDebugEnabled {
slog.Debug(fmt.Sprintf("← %s", response.Status))
}

var responseBody T
if err := json.NewDecoder(response.Body).Decode(&responseBody); err != nil {
var empty T
return NewResponse[T](response.StatusCode, &empty, fmt.Errorf("failed to read response Body: %w", err))
}

return NewResponse(response.StatusCode, &responseBody, nil)
}

func Post[T any, R any](c *HTTPClient, uri string, requestBody T) *Response[R] {
url := fmt.Sprintf("%s%s", c.baseUrl, uri)
requestBodyJson, err := json.Marshal(requestBody)
Expand Down
30 changes: 30 additions & 0 deletions cli/internal/client/model/metastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,36 @@ type DdlPage[T any] struct {
Content []T `json:"content"`
}

// MetastoreDumpPage is one page of the metastore dump, which the server returns as a Spring Page.
type MetastoreDumpPage struct {
Content []MetastoreDumpRow `json:"content"`
TotalElements int64 `json:"totalElements"`
NumberOfElements int `json:"numberOfElements"`
Last bool `json:"last"`
}

// MetastoreDumpRow deliberately omits the fields the census does not read.
//
// The server sends the whole encoded value in "v", plus a decoded property map, a timestamp and a
// direction. None of them are declared here, so the decoder walks past them instead of allocating a
// string per row for data that would be thrown away - the whole point is to count a table that is
// too large to hold.
type MetastoreDumpRow struct {
ID int64 `json:"id"`
K string `json:"k"`
Decoded *MetastoreDecodedEdge `json:"decoded"`
}

// MetastoreDecodedEdge is the server's decode of one row. Src and Tgt are typed as any because the
// server declares them as Object; metadata schemas make them strings, and anything else is still
// printable.
type MetastoreDecodedEdge struct {
Active bool `json:"active"`
Src any `json:"src"`
Tgt any `json:"tgt"`
LabelID int64 `json:"labelId"`
}

type StorageCreateRequest struct {
Desc string `json:"desc"`
Type string `json:"type"`
Expand Down
5 changes: 5 additions & 0 deletions cli/internal/command/command_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ var (
name: "migrate",
command: "migrate <plan [-o <file>]|apply [-i <file>]>",
}

TypeJdbcMetastore = Type{
name: "jdbc-metastore",
command: "jdbc-metastore <remaining> --capacity <n> [--page-size <n>] [--max-pages <n>] [--max-rows <n>]",
}
)

func (t Type) GetName() string {
Expand Down
Loading
Loading