forked from cerc-io/laconicd-deprecated
Graphql schema for DAG-JSON objects #1
@ -1,12 +1,5 @@
|
|||||||
# Reference to another record.
|
# Reference to another record.
|
||||||
type Reference {
|
scalar Link
|
||||||
id: String! # ID of linked record.
|
|
||||||
}
|
|
||||||
|
|
||||||
# Reference to another record.
|
|
||||||
input ReferenceInput {
|
|
||||||
id: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bonds contain funds that are used to pay rent on record registration and renewal.
|
# Bonds contain funds that are used to pay rent on record registration and renewal.
|
||||||
type Bond {
|
type Bond {
|
||||||
@ -37,44 +30,71 @@ type Account {
|
|||||||
balance: [Coin!] # Current balance for each coin type.
|
balance: [Coin!] # Current balance for each coin type.
|
||||||
}
|
}
|
||||||
|
|
||||||
# Value of a given type.
|
# Value describes a DAG-JSON compatible value.
|
||||||
type Value {
|
union Value =
|
||||||
null: Boolean
|
BooleanValue
|
||||||
|
| IntValue
|
||||||
|
| FloatValue
|
||||||
|
| StringValue
|
||||||
|
| BytesValue
|
||||||
|
| LinkValue
|
||||||
|
| ArrayValue
|
||||||
|
| MapValue
|
||||||
|
|
||||||
int: Int
|
type BooleanValue {
|
||||||
float: Float
|
value: Boolean!
|
||||||
string: String
|
|
||||||
boolean: Boolean
|
|
||||||
json: String
|
|
||||||
|
|
||||||
reference: Reference
|
|
||||||
|
|
||||||
values: [Value]
|
|
||||||
}
|
}
|
||||||
# Value of a given type used as input to queries.
|
|
||||||
input ValueInput {
|
|
||||||
null: Boolean
|
|
||||||
|
|
||||||
int: Int
|
type IntValue {
|
||||||
float: Float
|
value: Int!
|
||||||
string: String
|
}
|
||||||
boolean: Boolean
|
|
||||||
|
|
||||||
reference: ReferenceInput
|
type FloatValue {
|
||||||
|
value: Float!
|
||||||
|
}
|
||||||
|
|
||||||
values: [ValueInput]
|
type StringValue {
|
||||||
|
value: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type BytesValue {
|
||||||
|
value: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArrayValue {
|
||||||
|
value: [Value]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkValue {
|
||||||
|
value: Link!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MapValue {
|
||||||
|
value: [Attribute!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Key/value pair.
|
# Key/value pair.
|
||||||
type KeyValue {
|
type Attribute {
|
||||||
key: String!
|
key: String!
|
||||||
value: Value!
|
value: Value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Value of a given type used as input to queries.
|
||||||
|
# Note: GQL doesn't allow union input types.
|
||||||
|
input ValueInput {
|
||||||
|
int: Int
|
||||||
|
float: Float
|
||||||
|
string: String
|
||||||
|
boolean: Boolean
|
||||||
|
link: Link
|
||||||
|
array: [ValueInput]
|
||||||
|
map: [KeyValueInput!]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Key/value pair for inputs.
|
# Key/value pair for inputs.
|
||||||
input KeyValueInput {
|
input KeyValueInput {
|
||||||
key: String!
|
key: String!
|
||||||
value: ValueInput!
|
value: ValueInput
|
||||||
}
|
}
|
||||||
|
|
||||||
# Status information about a node (https://docs.tendermint.com/master/rpc/#/Info/status).
|
# Status information about a node (https://docs.tendermint.com/master/rpc/#/Info/status).
|
||||||
@ -155,7 +175,7 @@ type Record {
|
|||||||
createTime: String! # Record create time.
|
createTime: String! # Record create time.
|
||||||
expiryTime: String! # Record expiry time.
|
expiryTime: String! # Record expiry time.
|
||||||
owners: [String!] # Addresses of record owners.
|
owners: [String!] # Addresses of record owners.
|
||||||
attributes: [KeyValue] # Record attributes.
|
attributes: [Attribute!] # Record attributes.
|
||||||
references: [Record] # Record references.
|
references: [Record] # Record references.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +215,7 @@ type Query {
|
|||||||
getBondsByIds(ids: [String!]): [Bond]
|
getBondsByIds(ids: [String!]): [Bond]
|
||||||
|
|
||||||
# Query bonds.
|
# Query bonds.
|
||||||
queryBonds(attributes: [KeyValueInput]): [Bond]
|
queryBonds(attributes: [KeyValueInput!]): [Bond]
|
||||||
|
|
||||||
# Query bonds by owner.
|
# Query bonds by owner.
|
||||||
queryBondsByOwner(ownerAddresses: [String!]): [OwnerBonds]
|
queryBondsByOwner(ownerAddresses: [String!]): [OwnerBonds]
|
||||||
@ -210,7 +230,7 @@ type Query {
|
|||||||
# Query records.
|
# Query records.
|
||||||
queryRecords(
|
queryRecords(
|
||||||
# Multiple attribute conditions are in a logical AND.
|
# Multiple attribute conditions are in a logical AND.
|
||||||
attributes: [KeyValueInput]
|
attributes: [KeyValueInput!]
|
||||||
|
|
||||||
# Whether to query all records, not just named ones (false by default).
|
# Whether to query all records, not just named ones (false by default).
|
||||||
all: Boolean
|
all: Boolean
|
9860
gql/generated.go
9860
gql/generated.go
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,3 @@
|
|||||||
# .gqlgen.yml example
|
|
||||||
#
|
|
||||||
# Refer to https://gqlgen.com/config/
|
# Refer to https://gqlgen.com/config/
|
||||||
# for detailed .gqlgen.yml documentation.
|
# for detailed .gqlgen.yml documentation.
|
||||||
|
|
||||||
@ -12,3 +10,8 @@ model:
|
|||||||
resolver:
|
resolver:
|
||||||
filename: resolver.go
|
filename: resolver.go
|
||||||
type: Resolver
|
type: Resolver
|
||||||
|
|
||||||
|
models:
|
||||||
|
Link:
|
||||||
|
model:
|
||||||
|
- github.com/cerc-io/laconicd/gql.Link
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
package gql
|
package gql
|
||||||
|
|
||||||
|
type Value interface {
|
||||||
|
IsValue()
|
||||||
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
PubKey *string `json:"pubKey"`
|
PubKey *string `json:"pubKey"`
|
||||||
@ -10,6 +14,17 @@ type Account struct {
|
|||||||
Balance []*Coin `json:"balance"`
|
Balance []*Coin `json:"balance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArrayValue struct {
|
||||||
|
Value []Value `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ArrayValue) IsValue() {}
|
||||||
|
|
||||||
|
type Attribute struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value Value `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
type Auction struct {
|
type Auction struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@ -53,21 +68,52 @@ type Bond struct {
|
|||||||
Balance []*Coin `json:"balance"`
|
Balance []*Coin `json:"balance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BooleanValue struct {
|
||||||
|
Value bool `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BooleanValue) IsValue() {}
|
||||||
|
|
||||||
|
type BytesValue struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BytesValue) IsValue() {}
|
||||||
|
|
||||||
type Coin struct {
|
type Coin struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Quantity string `json:"quantity"`
|
Quantity string `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyValue struct {
|
type FloatValue struct {
|
||||||
Key string `json:"key"`
|
Value float64 `json:"value"`
|
||||||
Value *Value `json:"value"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (FloatValue) IsValue() {}
|
||||||
|
|
||||||
|
type IntValue struct {
|
||||||
|
Value int `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (IntValue) IsValue() {}
|
||||||
|
|
||||||
type KeyValueInput struct {
|
type KeyValueInput struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Value *ValueInput `json:"value"`
|
Value *ValueInput `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LinkValue struct {
|
||||||
|
Value Link `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LinkValue) IsValue() {}
|
||||||
|
|
||||||
|
type MapValue struct {
|
||||||
|
Value []*Attribute `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MapValue) IsValue() {}
|
||||||
|
|
||||||
type NameRecord struct {
|
type NameRecord struct {
|
||||||
Latest *NameRecordEntry `json:"latest"`
|
Latest *NameRecordEntry `json:"latest"`
|
||||||
History []*NameRecordEntry `json:"history"`
|
History []*NameRecordEntry `json:"history"`
|
||||||
@ -102,18 +148,10 @@ type Record struct {
|
|||||||
CreateTime string `json:"createTime"`
|
CreateTime string `json:"createTime"`
|
||||||
ExpiryTime string `json:"expiryTime"`
|
ExpiryTime string `json:"expiryTime"`
|
||||||
Owners []string `json:"owners"`
|
Owners []string `json:"owners"`
|
||||||
Attributes []*KeyValue `json:"attributes"`
|
Attributes []*Attribute `json:"attributes"`
|
||||||
References []*Record `json:"references"`
|
References []*Record `json:"references"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Reference struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReferenceInput struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Status struct {
|
type Status struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Node *NodeInfo `json:"node"`
|
Node *NodeInfo `json:"node"`
|
||||||
@ -125,6 +163,12 @@ type Status struct {
|
|||||||
DiskUsage string `json:"disk_usage"`
|
DiskUsage string `json:"disk_usage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StringValue struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (StringValue) IsValue() {}
|
||||||
|
|
||||||
type SyncInfo struct {
|
type SyncInfo struct {
|
||||||
LatestBlockHash string `json:"latest_block_hash"`
|
LatestBlockHash string `json:"latest_block_hash"`
|
||||||
LatestBlockHeight string `json:"latest_block_height"`
|
LatestBlockHeight string `json:"latest_block_height"`
|
||||||
@ -138,23 +182,12 @@ type ValidatorInfo struct {
|
|||||||
ProposerPriority *string `json:"proposer_priority"`
|
ProposerPriority *string `json:"proposer_priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Value struct {
|
|
||||||
Null *bool `json:"null"`
|
|
||||||
Int *int `json:"int"`
|
|
||||||
Float *float64 `json:"float"`
|
|
||||||
String *string `json:"string"`
|
|
||||||
Boolean *bool `json:"boolean"`
|
|
||||||
JSON *string `json:"json"`
|
|
||||||
Reference *Reference `json:"reference"`
|
|
||||||
Values []*Value `json:"values"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ValueInput struct {
|
type ValueInput struct {
|
||||||
Null *bool `json:"null"`
|
|
||||||
Int *int `json:"int"`
|
Int *int `json:"int"`
|
||||||
Float *float64 `json:"float"`
|
Float *float64 `json:"float"`
|
||||||
String *string `json:"string"`
|
String *string `json:"string"`
|
||||||
Boolean *bool `json:"boolean"`
|
Boolean *bool `json:"boolean"`
|
||||||
Reference *ReferenceInput `json:"reference"`
|
Link *Link `json:"link"`
|
||||||
Values []*ValueInput `json:"values"`
|
Array []*ValueInput `json:"array"`
|
||||||
|
Map []*KeyValueInput `json:"map"`
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ func (q queryResolver) QueryRecords(ctx context.Context, attributes []*KeyValueI
|
|||||||
res, err := nsQueryClient.ListRecords(
|
res, err := nsQueryClient.ListRecords(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
®istrytypes.QueryListRecordsRequest{
|
®istrytypes.QueryListRecordsRequest{
|
||||||
Attributes: parseRequestAttributes(attributes),
|
Attributes: toRPCAttributes(attributes),
|
||||||
All: (all != nil && *all),
|
All: (all != nil && *all),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
33
gql/scalar.go
Normal file
33
gql/scalar.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package gql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Represents an IPLD link. Links are generally but not necessarily implemented as CIDs
|
||||||
|
type Link string
|
||||||
|
|
||||||
|
func (l Link) String() string {
|
||||||
|
return string(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalGQLContext implements the graphql.ContextUnmarshaler interface
|
||||||
|
func (l *Link) UnmarshalGQLContext(_ context.Context, v interface{}) error {
|
||||||
|
s, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Link must be a string")
|
||||||
|
}
|
||||||
|
*l = Link(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalGQLContext implements the graphql.ContextMarshaler interface
|
||||||
|
func (l Link) MarshalGQLContext(_ context.Context, w io.Writer) error {
|
||||||
|
encodable := map[string]string{
|
||||||
|
"/": l.String(),
|
||||||
|
}
|
||||||
|
return json.NewEncoder(w).Encode(encodable)
|
||||||
|
}
|
254
gql/util.go
254
gql/util.go
@ -2,15 +2,15 @@ package gql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"fmt" // #nosec G702
|
||||||
"fmt"
|
|
||||||
"reflect" // #nosec G702
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
auctiontypes "github.com/cerc-io/laconicd/x/auction/types"
|
auctiontypes "github.com/cerc-io/laconicd/x/auction/types"
|
||||||
bondtypes "github.com/cerc-io/laconicd/x/bond/types"
|
bondtypes "github.com/cerc-io/laconicd/x/bond/types"
|
||||||
registrytypes "github.com/cerc-io/laconicd/x/registry/types"
|
registrytypes "github.com/cerc-io/laconicd/x/registry/types"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OwnerAttributeName denotes the owner attribute name for a bond.
|
// OwnerAttributeName denotes the owner attribute name for a bond.
|
||||||
@ -61,13 +61,21 @@ func getGQLRecord(ctx context.Context, resolver QueryResolver, record registryty
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
recordType := record.ToReadableRecord()
|
node, err := ipld.Decode(record.Attributes, dagjson.Decode)
|
||||||
attributes, err := getAttributes(&recordType)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if node.Kind() != ipld.Kind_Map {
|
||||||
|
return nil, fmt.Errorf("invalid record attributes")
|
||||||
|
}
|
||||||
|
|
||||||
|
var links []string
|
||||||
|
attributes, err := resolveIPLDNode(node, &links)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
references, err := getReferences(ctx, resolver, &recordType)
|
references, err := resolver.GetRecordsByIds(ctx, links)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -79,11 +87,96 @@ func getGQLRecord(ctx context.Context, resolver QueryResolver, record registryty
|
|||||||
ExpiryTime: record.GetExpiryTime(),
|
ExpiryTime: record.GetExpiryTime(),
|
||||||
Owners: record.GetOwners(),
|
Owners: record.GetOwners(),
|
||||||
Names: record.GetNames(),
|
Names: record.GetNames(),
|
||||||
Attributes: attributes,
|
Attributes: attributes.(MapValue).Value,
|
||||||
References: references,
|
References: references,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveIPLDNode(node ipld.Node, links *[]string) (Value, error) {
|
||||||
|
switch node.Kind() {
|
||||||
|
case ipld.Kind_Map:
|
||||||
|
var entries []*Attribute
|
||||||
|
for itr := node.MapIterator(); !itr.Done(); {
|
||||||
|
k, v, err := itr.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if k.Kind() != ipld.Kind_String {
|
||||||
|
return nil, fmt.Errorf("invalid record attribute key type: %s", k.Kind())
|
||||||
|
}
|
||||||
|
s, err := k.AsString()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
val, err := resolveIPLDNode(v, links)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries = append(entries, &Attribute{
|
||||||
|
Key: s,
|
||||||
|
Value: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return MapValue{entries}, nil
|
||||||
|
case ipld.Kind_List:
|
||||||
|
var values []Value
|
||||||
|
for itr := node.ListIterator(); !itr.Done(); {
|
||||||
|
_, v, err := itr.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
val, err := resolveIPLDNode(v, links)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
values = append(values, val)
|
||||||
|
}
|
||||||
|
return ArrayValue{values}, nil
|
||||||
|
case ipld.Kind_Null:
|
||||||
|
return nil, nil
|
||||||
|
case ipld.Kind_Bool:
|
||||||
|
val, err := node.AsBool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return BooleanValue{val}, nil
|
||||||
|
case ipld.Kind_Int:
|
||||||
|
val, err := node.AsInt()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// TODO: handle bigger ints
|
||||||
|
return IntValue{int(val)}, nil
|
||||||
|
case ipld.Kind_Float:
|
||||||
|
val, err := node.AsFloat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return FloatValue{val}, nil
|
||||||
|
case ipld.Kind_String:
|
||||||
|
val, err := node.AsString()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return StringValue{val}, nil
|
||||||
|
case ipld.Kind_Bytes:
|
||||||
|
val, err := node.AsBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return BytesValue{string(val)}, nil
|
||||||
|
case ipld.Kind_Link:
|
||||||
|
val, err := node.AsLink()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
*links = append(*links, val.String())
|
||||||
|
return LinkValue{Link(val.String())}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid node kind")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getGQLNameRecord(record *registrytypes.NameRecord) (*NameRecord, error) {
|
func getGQLNameRecord(record *registrytypes.NameRecord) (*NameRecord, error) {
|
||||||
if record == nil {
|
if record == nil {
|
||||||
return nil, fmt.Errorf("got nil record")
|
return nil, fmt.Errorf("got nil record")
|
||||||
@ -163,136 +256,47 @@ func GetGQLAuction(auction *auctiontypes.Auction, bids []*auctiontypes.Bid) (*Au
|
|||||||
return &gqlAuction, nil
|
return &gqlAuction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getReferences(ctx context.Context, resolver QueryResolver, r *registrytypes.ReadableRecord) ([]*Record, error) {
|
func toRPCValue(value *ValueInput) *registrytypes.QueryListRecordsRequest_ValueInput {
|
||||||
var ids []string
|
var rpcval registrytypes.QueryListRecordsRequest_ValueInput
|
||||||
|
|
||||||
// #nosec G705
|
switch {
|
||||||
for key := range r.Attributes {
|
case value == nil:
|
||||||
//nolint: all
|
return nil
|
||||||
switch r.Attributes[key].(type) {
|
case value.Int != nil:
|
||||||
case interface{}:
|
rpcval.Value = ®istrytypes.QueryListRecordsRequest_ValueInput_Int{Int: int64(*value.Int)}
|
||||||
if obj, ok := r.Attributes[key].(map[string]interface{}); ok {
|
case value.Float != nil:
|
||||||
if _, ok := obj["/"]; ok && len(obj) == 1 {
|
rpcval.Value = ®istrytypes.QueryListRecordsRequest_ValueInput_Float{Float: *value.Float}
|
||||||
if _, ok := obj["/"].(string); ok {
|
case value.String != nil:
|
||||||
ids = append(ids, obj["/"].(string))
|
rpcval.Value = ®istrytypes.QueryListRecordsRequest_ValueInput_String_{String_: *value.String}
|
||||||
|
case value.Boolean != nil:
|
||||||
|
rpcval.Value = ®istrytypes.QueryListRecordsRequest_ValueInput_Boolean{Boolean: *value.Boolean}
|
||||||
|
case value.Link != nil:
|
||||||
|
rpcval.Value = ®istrytypes.QueryListRecordsRequest_ValueInput_Link{Link: value.Link.String()}
|
||||||
|
case value.Array != nil:
|
||||||
|
var contents registrytypes.QueryListRecordsRequest_ArrayInput
|
||||||
|
for _, val := range value.Array {
|
||||||
|
contents.Values = append(contents.Values, toRPCValue(val))
|
||||||
}
|
}
|
||||||
|
rpcval.Value = ®istrytypes.QueryListRecordsRequest_ValueInput_Array{Array: &contents}
|
||||||
|
case value.Map != nil:
|
||||||
|
var contents registrytypes.QueryListRecordsRequest_MapInput
|
||||||
|
for _, kv := range value.Map {
|
||||||
|
contents.Values[kv.Key] = toRPCValue(kv.Value)
|
||||||
}
|
}
|
||||||
|
rpcval.Value = ®istrytypes.QueryListRecordsRequest_ValueInput_Map{Map: &contents}
|
||||||
}
|
}
|
||||||
}
|
return &rpcval
|
||||||
}
|
|
||||||
|
|
||||||
return resolver.GetRecordsByIds(ctx, ids)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAttributes(r *registrytypes.ReadableRecord) ([]*KeyValue, error) {
|
func toRPCAttributes(attrs []*KeyValueInput) []*registrytypes.QueryListRecordsRequest_KeyValueInput {
|
||||||
return mapToKeyValuePairs(r.Attributes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapToKeyValuePairs(attrs map[string]interface{}) ([]*KeyValue, error) {
|
|
||||||
kvPairs := []*KeyValue{}
|
|
||||||
|
|
||||||
trueVal := true
|
|
||||||
falseVal := false
|
|
||||||
|
|
||||||
// #nosec G705
|
|
||||||
for key, value := range attrs {
|
|
||||||
kvPair := &KeyValue{
|
|
||||||
Key: key,
|
|
||||||
Value: &Value{},
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val := value.(type) {
|
|
||||||
case nil:
|
|
||||||
kvPair.Value.Null = &trueVal
|
|
||||||
case int:
|
|
||||||
kvPair.Value.Int = &val
|
|
||||||
case float64:
|
|
||||||
kvPair.Value.Float = &val
|
|
||||||
case string:
|
|
||||||
kvPair.Value.String = &val
|
|
||||||
case bool:
|
|
||||||
kvPair.Value.Boolean = &val
|
|
||||||
case interface{}:
|
|
||||||
if obj, ok := value.(map[string]interface{}); ok {
|
|
||||||
if _, ok := obj["/"]; ok && len(obj) == 1 {
|
|
||||||
if _, ok := obj["/"].(string); ok {
|
|
||||||
kvPair.Value.Reference = &Reference{
|
|
||||||
ID: obj["/"].(string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bytes, err := json.Marshal(obj)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonStr := string(bytes)
|
|
||||||
kvPair.Value.JSON = &jsonStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if kvPair.Value.Null == nil {
|
|
||||||
kvPair.Value.Null = &falseVal
|
|
||||||
}
|
|
||||||
|
|
||||||
valueType := reflect.ValueOf(value)
|
|
||||||
if valueType.Kind() == reflect.Slice {
|
|
||||||
bytes, err := json.Marshal(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonStr := string(bytes)
|
|
||||||
kvPair.Value.JSON = &jsonStr
|
|
||||||
}
|
|
||||||
|
|
||||||
kvPairs = append(kvPairs, kvPair)
|
|
||||||
}
|
|
||||||
|
|
||||||
return kvPairs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRequestAttributes(attrs []*KeyValueInput) []*registrytypes.QueryListRecordsRequest_KeyValueInput {
|
|
||||||
kvPairs := []*registrytypes.QueryListRecordsRequest_KeyValueInput{}
|
kvPairs := []*registrytypes.QueryListRecordsRequest_KeyValueInput{}
|
||||||
|
|
||||||
for _, value := range attrs {
|
for _, value := range attrs {
|
||||||
|
parsedValue := toRPCValue(value.Value)
|
||||||
kvPair := ®istrytypes.QueryListRecordsRequest_KeyValueInput{
|
kvPair := ®istrytypes.QueryListRecordsRequest_KeyValueInput{
|
||||||
Key: value.Key,
|
Key: value.Key,
|
||||||
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{},
|
Value: parsedValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.Value.String != nil {
|
|
||||||
kvPair.Value.String_ = *value.Value.String
|
|
||||||
kvPair.Value.Type = "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.Value.Int != nil {
|
|
||||||
kvPair.Value.Int = int64(*value.Value.Int)
|
|
||||||
kvPair.Value.Type = "int"
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.Value.Float != nil {
|
|
||||||
kvPair.Value.Float = *value.Value.Float
|
|
||||||
kvPair.Value.Type = "float"
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.Value.Boolean != nil {
|
|
||||||
kvPair.Value.Boolean = *value.Value.Boolean
|
|
||||||
kvPair.Value.Type = "boolean"
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.Value.Reference != nil {
|
|
||||||
reference := ®istrytypes.QueryListRecordsRequest_ReferenceInput{
|
|
||||||
Id: value.Value.Reference.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
kvPair.Value.Reference = reference
|
|
||||||
kvPair.Value.Type = "reference"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Handle arrays.
|
|
||||||
|
|
||||||
kvPairs = append(kvPairs, kvPair)
|
kvPairs = append(kvPairs, kvPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,17 +67,26 @@ message QueryParamsResponse {
|
|||||||
|
|
||||||
// QueryListRecordsRequest is request type for registry records list
|
// QueryListRecordsRequest is request type for registry records list
|
||||||
message QueryListRecordsRequest {
|
message QueryListRecordsRequest {
|
||||||
message ReferenceInput {
|
message LinkInput {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
}
|
}
|
||||||
|
message ArrayInput {
|
||||||
|
repeated ValueInput values = 1;
|
||||||
|
}
|
||||||
|
message MapInput {
|
||||||
|
map<string, ValueInput> values = 1;
|
||||||
|
}
|
||||||
message ValueInput {
|
message ValueInput {
|
||||||
string type = 1;
|
// Type of record attribute value
|
||||||
string string = 2;
|
oneof value {
|
||||||
int64 int = 3;
|
string string = 1;
|
||||||
double float = 4;
|
int64 int = 2;
|
||||||
bool boolean = 5;
|
double float = 3;
|
||||||
ReferenceInput reference = 6;
|
bool boolean = 4;
|
||||||
repeated ValueInput values = 7;
|
string link = 5;
|
||||||
|
ArrayInput array = 6;
|
||||||
|
MapInput map = 7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
message KeyValueInput {
|
message KeyValueInput {
|
||||||
string key = 1;
|
string key = 1;
|
||||||
|
@ -47,24 +47,20 @@ func Int64ToBytes(num int64) []byte {
|
|||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalMapToJSONBytes converts map[string]interface{} to bytes.
|
func MustMarshalJSON[T any](val T) (bytes []byte) {
|
||||||
func MarshalMapToJSONBytes(val map[string]interface{}) (bytes []byte) {
|
|
||||||
bytes, err := json.Marshal(val)
|
bytes, err := json.Marshal(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("Marshal error.")
|
panic("JSON marshal error:" + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnMarshalMapFromJSONBytes converts bytes to map[string]interface{}.
|
func MustUnmarshalJSON[T any](bytes []byte) T {
|
||||||
func UnMarshalMapFromJSONBytes(bytes []byte) map[string]interface{} {
|
var val T
|
||||||
var val map[string]interface{}
|
|
||||||
err := json.Unmarshal(bytes, &val)
|
err := json.Unmarshal(bytes, &val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("Unmarshal error.")
|
panic("JSON unmarshal error:" + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,12 +3,13 @@ package keeper_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/cerc-io/laconicd/x/registry/client/cli"
|
"github.com/cerc-io/laconicd/x/registry/client/cli"
|
||||||
"github.com/cerc-io/laconicd/x/registry/helpers"
|
"github.com/cerc-io/laconicd/x/registry/helpers"
|
||||||
"github.com/cerc-io/laconicd/x/registry/keeper"
|
"github.com/cerc-io/laconicd/x/registry/keeper"
|
||||||
registrytypes "github.com/cerc-io/laconicd/x/registry/types"
|
registrytypes "github.com/cerc-io/laconicd/x/registry/types"
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (suite *KeeperTestSuite) TestGrpcQueryParams() {
|
func (suite *KeeperTestSuite) TestGrpcQueryParams() {
|
||||||
@ -69,26 +70,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
|
|||||||
{
|
{
|
||||||
Key: "type",
|
Key: "type",
|
||||||
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
||||||
Type: "string",
|
Value: ®istrytypes.QueryListRecordsRequest_ValueInput_String_{"WebsiteRegistrationRecord"},
|
||||||
String_: "WebsiteRegistrationRecord",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
All: true,
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Filter with tag (extant) (https://git.vdb.to/cerc-io/laconicd/issues/129)",
|
|
||||||
®istrytypes.QueryListRecordsRequest{
|
|
||||||
Attributes: []*registrytypes.QueryListRecordsRequest_KeyValueInput{
|
|
||||||
{
|
|
||||||
Key: "tags",
|
|
||||||
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
|
||||||
Type: "string",
|
|
||||||
String_: "tagA",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -98,6 +80,35 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
|
|||||||
false,
|
false,
|
||||||
1,
|
1,
|
||||||
},
|
},
|
||||||
|
// Skip the following test as querying with recursive values not supported (PR https://git.vdb.to/cerc-io/laconicd/pulls/112)
|
||||||
|
// See function RecordsFromAttributes (QueryValueToJSON call) in the registry keeper implementation (x/registry/keeper/keeper.go)
|
||||||
|
// {
|
||||||
|
// "Filter with tag (extant) (https://git.vdb.to/cerc-io/laconicd/issues/129)",
|
||||||
|
// ®istrytypes.QueryListRecordsRequest{
|
||||||
|
// Attributes: []*registrytypes.QueryListRecordsRequest_KeyValueInput{
|
||||||
|
// {
|
||||||
|
// Key: "tags",
|
||||||
|
// // Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
||||||
|
// // Value: ®istrytypes.QueryListRecordsRequest_ValueInput_String_{"tagA"},
|
||||||
|
// // },
|
||||||
|
// Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
||||||
|
// Value: ®istrytypes.QueryListRecordsRequest_ValueInput_Array{Array: ®istrytypes.QueryListRecordsRequest_ArrayInput{
|
||||||
|
// Values: []*registrytypes.QueryListRecordsRequest_ValueInput{
|
||||||
|
// {
|
||||||
|
// Value: ®istrytypes.QueryListRecordsRequest_ValueInput_String_{"tagA"},
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }},
|
||||||
|
// },
|
||||||
|
// // Throws: "Recursive query values are not supported"
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// All: true,
|
||||||
|
// },
|
||||||
|
// true,
|
||||||
|
// false,
|
||||||
|
// 1,
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
"Filter with tag (non-existent) (https://git.vdb.to/cerc-io/laconicd/issues/129)",
|
"Filter with tag (non-existent) (https://git.vdb.to/cerc-io/laconicd/issues/129)",
|
||||||
®istrytypes.QueryListRecordsRequest{
|
®istrytypes.QueryListRecordsRequest{
|
||||||
@ -105,8 +116,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
|
|||||||
{
|
{
|
||||||
Key: "tags",
|
Key: "tags",
|
||||||
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
||||||
Type: "string",
|
Value: ®istrytypes.QueryListRecordsRequest_ValueInput_String_{"NOEXIST"},
|
||||||
String_: "NOEXIST",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -123,8 +133,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
|
|||||||
{
|
{
|
||||||
Key: "typ",
|
Key: "typ",
|
||||||
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
||||||
Type: "string",
|
Value: ®istrytypes.QueryListRecordsRequest_ValueInput_String_{"eWebsiteRegistrationRecord"},
|
||||||
String_: "eWebsiteRegistrationRecord",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -141,8 +150,7 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
|
|||||||
{
|
{
|
||||||
Key: "x500state_name",
|
Key: "x500state_name",
|
||||||
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
Value: ®istrytypes.QueryListRecordsRequest_ValueInput{
|
||||||
Type: "string",
|
Value: ®istrytypes.QueryListRecordsRequest_ValueInput_String_{"california"},
|
||||||
String_: "california",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -183,9 +191,13 @@ func (suite *KeeperTestSuite) TestGrpcGetRecordLists() {
|
|||||||
sr.Equal(resp.GetRecords()[0].GetBondId(), suite.bond.GetId())
|
sr.Equal(resp.GetRecords()[0].GetBondId(), suite.bond.GetId())
|
||||||
|
|
||||||
for _, record := range resp.GetRecords() {
|
for _, record := range resp.GetRecords() {
|
||||||
recAttr := helpers.UnMarshalMapFromJSONBytes(record.Attributes)
|
recAttr := helpers.MustUnmarshalJSON[registrytypes.AttributeMap](record.Attributes)
|
||||||
|
|
||||||
for _, attr := range test.req.GetAttributes() {
|
for _, attr := range test.req.GetAttributes() {
|
||||||
av := keeper.GetAttributeValue(attr.Value)
|
enc, err := keeper.QueryValueToJSON(attr.Value)
|
||||||
|
sr.NoError(err)
|
||||||
|
av := helpers.MustUnmarshalJSON[any](enc)
|
||||||
|
|
||||||
if nil != av && nil != recAttr[attr.Key] &&
|
if nil != av && nil != recAttr[attr.Key] &&
|
||||||
reflect.Slice == reflect.TypeOf(recAttr[attr.Key]).Kind() &&
|
reflect.Slice == reflect.TypeOf(recAttr[attr.Key]).Kind() &&
|
||||||
reflect.Slice != reflect.TypeOf(av).Kind() {
|
reflect.Slice != reflect.TypeOf(av).Kind() {
|
||||||
|
@ -8,10 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
errorsmod "cosmossdk.io/errors"
|
errorsmod "cosmossdk.io/errors"
|
||||||
auctionkeeper "github.com/cerc-io/laconicd/x/auction/keeper"
|
|
||||||
bondkeeper "github.com/cerc-io/laconicd/x/bond/keeper"
|
|
||||||
"github.com/cerc-io/laconicd/x/registry/helpers"
|
|
||||||
"github.com/cerc-io/laconicd/x/registry/types"
|
|
||||||
"github.com/cosmos/cosmos-sdk/codec"
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
"github.com/cosmos/cosmos-sdk/codec/legacy"
|
"github.com/cosmos/cosmos-sdk/codec/legacy"
|
||||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||||
@ -20,7 +16,18 @@ import (
|
|||||||
auth "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
auth "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
||||||
bank "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
bank "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||||
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
|
||||||
|
"github.com/gibson042/canonicaljson-go"
|
||||||
|
cid "github.com/ipfs/go-cid"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||||
|
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||||
"github.com/tendermint/tendermint/libs/log"
|
"github.com/tendermint/tendermint/libs/log"
|
||||||
|
|
||||||
|
auctionkeeper "github.com/cerc-io/laconicd/x/auction/keeper"
|
||||||
|
bondkeeper "github.com/cerc-io/laconicd/x/bond/keeper"
|
||||||
|
"github.com/cerc-io/laconicd/x/registry/helpers"
|
||||||
|
"github.com/cerc-io/laconicd/x/registry/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -99,6 +106,10 @@ func NewKeeper(cdc codec.BinaryCodec, accountKeeper auth.AccountKeeper, bankKeep
|
|||||||
|
|
||||||
// Logger returns a module-specific logger.
|
// Logger returns a module-specific logger.
|
||||||
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
|
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
|
||||||
|
return logger(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logger(ctx sdk.Context) log.Logger {
|
||||||
return ctx.Logger().With("module", types.ModuleName)
|
return ctx.Logger().With("module", types.ModuleName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,8 +157,11 @@ func (k Keeper) ListRecords(ctx sdk.Context) []types.Record {
|
|||||||
func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*types.QueryListRecordsRequest_KeyValueInput, all bool) ([]types.Record, error) {
|
func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*types.QueryListRecordsRequest_KeyValueInput, all bool) ([]types.Record, error) {
|
||||||
resultRecordIds := []string{}
|
resultRecordIds := []string{}
|
||||||
for i, attr := range attributes {
|
for i, attr := range attributes {
|
||||||
val := GetAttributeValue(attr.Value)
|
suffix, err := QueryValueToJSON(attr.Value)
|
||||||
attributeIndex := GetAttributesIndexKey(attr.Key, val)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
attributeIndex := GetAttributesIndexKey(attr.Key, suffix)
|
||||||
recordIds, err := k.GetAttributeMapping(ctx, attributeIndex)
|
recordIds, err := k.GetAttributeMapping(ctx, attributeIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -175,23 +189,53 @@ func (k Keeper) RecordsFromAttributes(ctx sdk.Context, attributes []*types.Query
|
|||||||
return records, nil
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAttributeValue(input *types.QueryListRecordsRequest_ValueInput) interface{} {
|
// TODO not recursive, and only should be if we want to support querying with whole sub-objects,
|
||||||
if input.Type == "int" {
|
// which seems unnecessary.
|
||||||
return input.GetInt()
|
func QueryValueToJSON(input *types.QueryListRecordsRequest_ValueInput) ([]byte, error) {
|
||||||
|
np := basicnode.Prototype.Any
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
|
||||||
|
switch value := input.GetValue().(type) {
|
||||||
|
case *types.QueryListRecordsRequest_ValueInput_String_:
|
||||||
|
err := nb.AssignString(value.String_)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if input.Type == "float" {
|
case *types.QueryListRecordsRequest_ValueInput_Int:
|
||||||
return input.GetFloat()
|
err := nb.AssignInt(value.Int)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if input.Type == "string" {
|
case *types.QueryListRecordsRequest_ValueInput_Float:
|
||||||
return input.GetString_()
|
err := nb.AssignFloat(value.Float)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if input.Type == "boolean" {
|
case *types.QueryListRecordsRequest_ValueInput_Boolean:
|
||||||
return input.GetBoolean()
|
err := nb.AssignBool(value.Boolean)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if input.Type == "reference" {
|
case *types.QueryListRecordsRequest_ValueInput_Link:
|
||||||
return input.GetReference().GetId()
|
link := cidlink.Link{Cid: cid.MustParse(value.Link)}
|
||||||
|
err := nb.AssignLink(link)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil
|
case *types.QueryListRecordsRequest_ValueInput_Array:
|
||||||
|
return nil, fmt.Errorf("recursive query values are not supported")
|
||||||
|
case *types.QueryListRecordsRequest_ValueInput_Map:
|
||||||
|
return nil, fmt.Errorf("recursive query values are not supported")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("value has unexpected type %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
n := nb.Build()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := dagjson.Encode(n, &buf); err != nil {
|
||||||
|
return nil, fmt.Errorf("encoding value to JSON failed: %w", err)
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIntersection(a []string, b []string) []string {
|
func getIntersection(a []string, b []string) []string {
|
||||||
@ -244,7 +288,7 @@ func (k Keeper) GetRecordExpiryQueue(ctx sdk.Context) []*types.ExpiryQueueRecord
|
|||||||
// ProcessSetRecord creates a record.
|
// ProcessSetRecord creates a record.
|
||||||
func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*types.ReadableRecord, error) {
|
func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*types.ReadableRecord, error) {
|
||||||
payload := msg.Payload.ToReadablePayload()
|
payload := msg.Payload.ToReadablePayload()
|
||||||
record := types.ReadableRecord{Attributes: payload.Record, BondID: msg.BondId}
|
record := types.ReadableRecord{Attributes: payload.RecordAttributes, BondID: msg.BondId}
|
||||||
|
|
||||||
// Check signatures.
|
// Check signatures.
|
||||||
resourceSignBytes, _ := record.GetSignBytes()
|
resourceSignBytes, _ := record.GetSignBytes()
|
||||||
@ -264,14 +308,12 @@ func (k Keeper) ProcessSetRecord(ctx sdk.Context, msg types.MsgSetRecord) (*type
|
|||||||
for _, sig := range payload.Signatures {
|
for _, sig := range payload.Signatures {
|
||||||
pubKey, err := legacy.PubKeyFromBytes(helpers.BytesFromBase64(sig.PubKey))
|
pubKey, err := legacy.PubKeyFromBytes(helpers.BytesFromBase64(sig.PubKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error decoding pubKey from bytes: ", err)
|
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Error decoding pubKey from bytes: ", err))
|
||||||
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid public key.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig))
|
sigOK := pubKey.VerifySignature(resourceSignBytes, helpers.BytesFromBase64(sig.Sig))
|
||||||
if !sigOK {
|
if !sigOK {
|
||||||
fmt.Println("Signature mismatch: ", sig.PubKey)
|
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprint("Signature mismatch: ", sig.PubKey))
|
||||||
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "Invalid signature.")
|
|
||||||
}
|
}
|
||||||
record.Owners = append(record.Owners, pubKey.Address().String())
|
record.Owners = append(record.Owners, pubKey.Address().String())
|
||||||
}
|
}
|
||||||
@ -312,7 +354,7 @@ func (k Keeper) processRecord(ctx sdk.Context, record *types.ReadableRecord, isR
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, record.ExpiryTime)
|
expiryTimeKey := GetAttributesIndexKey(ExpiryTimeAttributeName, []byte(record.ExpiryTime))
|
||||||
if err := k.SetAttributeMapping(ctx, expiryTimeKey, record.ID); err != nil {
|
if err := k.SetAttributeMapping(ctx, expiryTimeKey, record.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -334,14 +376,51 @@ func (k Keeper) PutRecord(ctx sdk.Context, record types.Record) {
|
|||||||
k.updateBlockChangeSetForRecord(ctx, record.Id)
|
k.updateBlockChangeSetForRecord(ctx, record.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k Keeper) processAttributes(ctx sdk.Context, attrs map[string]any, id string, prefix string) error {
|
func (k Keeper) processAttributes(ctx sdk.Context, attrs types.AttributeMap, id string, prefix string) error {
|
||||||
for key, value := range attrs {
|
np := basicnode.Prototype.Map
|
||||||
if subRecord, ok := value.(map[string]any); ok {
|
nb := np.NewBuilder()
|
||||||
err := k.processAttributes(ctx, subRecord, id, key)
|
encAttrs, err := canonicaljson.Marshal(attrs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(attrs) == 0 {
|
||||||
|
encAttrs = []byte("{}")
|
||||||
|
}
|
||||||
|
err = dagjson.Decode(nb, bytes.NewReader(encAttrs))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode attributes: %w", err)
|
||||||
|
}
|
||||||
|
n := nb.Build()
|
||||||
|
if n.Kind() != ipld.Kind_Map {
|
||||||
|
return fmt.Errorf("record attributes must be a map, not %T", n.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
return k.processAttributeMap(ctx, n, id, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keeper) processAttributeMap(ctx sdk.Context, n ipld.Node, id string, prefix string) error {
|
||||||
|
for it := n.MapIterator(); !it.Done(); {
|
||||||
|
//nolint:misspell
|
||||||
|
keynode, valuenode, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key, err := keynode.AsString()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if valuenode.Kind() == ipld.Kind_Map {
|
||||||
|
err := k.processAttributeMap(ctx, valuenode, id, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := dagjson.Encode(valuenode, &buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value := buf.Bytes()
|
||||||
indexKey := GetAttributesIndexKey(prefix+key, value)
|
indexKey := GetAttributesIndexKey(prefix+key, value)
|
||||||
if err := k.SetAttributeMapping(ctx, indexKey, id); err != nil {
|
if err := k.SetAttributeMapping(ctx, indexKey, id); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -351,8 +430,8 @@ func (k Keeper) processAttributes(ctx sdk.Context, attrs map[string]any, id stri
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAttributesIndexKey(key string, value interface{}) []byte {
|
func GetAttributesIndexKey(key string, suffix []byte) []byte {
|
||||||
keyString := fmt.Sprintf("%s=%s", key, value)
|
keyString := fmt.Sprintf("%s=%s", key, suffix)
|
||||||
return append(PrefixAttributesIndex, []byte(keyString)...)
|
return append(PrefixAttributesIndex, []byte(keyString)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,8 +443,6 @@ func (k Keeper) SetAttributeMapping(ctx sdk.Context, key []byte, recordID string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot unmarshal byte array, error, %w", err)
|
return fmt.Errorf("cannot unmarshal byte array, error, %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
recordIds = []string{}
|
|
||||||
}
|
}
|
||||||
recordIds = append(recordIds, recordID)
|
recordIds = append(recordIds, recordID)
|
||||||
bz, err := json.Marshal(recordIds)
|
bz, err := json.Marshal(recordIds)
|
||||||
|
@ -611,7 +611,7 @@ func (k Keeper) ProcessAuthorityExpiryQueue(ctx sdk.Context) {
|
|||||||
k.SetNameAuthority(ctx, name, &authority)
|
k.SetNameAuthority(ctx, name, &authority)
|
||||||
k.DeleteAuthorityExpiryQueue(ctx, name, authority)
|
k.DeleteAuthorityExpiryQueue(ctx, name, authority)
|
||||||
|
|
||||||
ctx.Logger().Info(fmt.Sprintf("Marking authority expired as no bond present: %s", name))
|
k.Logger(ctx).Info(fmt.Sprintf("Marking authority expired as no bond present: %s", name))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -672,7 +672,7 @@ func (k Keeper) AuthorityExpiryQueueIterator(ctx sdk.Context, endTime time.Time)
|
|||||||
|
|
||||||
// TryTakeAuthorityRent tries to take rent from the authority bond.
|
// TryTakeAuthorityRent tries to take rent from the authority bond.
|
||||||
func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority types.NameAuthority) {
|
func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority types.NameAuthority) {
|
||||||
ctx.Logger().Info(fmt.Sprintf("Trying to take rent for authority: %s", name))
|
k.Logger(ctx).Info(fmt.Sprintf("Trying to take rent for authority: %s", name))
|
||||||
|
|
||||||
params := k.GetParams(ctx)
|
params := k.GetParams(ctx)
|
||||||
rent := params.AuthorityRent
|
rent := params.AuthorityRent
|
||||||
@ -684,7 +684,7 @@ func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority typ
|
|||||||
k.SetNameAuthority(ctx, name, &authority)
|
k.SetNameAuthority(ctx, name, &authority)
|
||||||
k.DeleteAuthorityExpiryQueue(ctx, name, authority)
|
k.DeleteAuthorityExpiryQueue(ctx, name, authority)
|
||||||
|
|
||||||
ctx.Logger().Info(fmt.Sprintf("Insufficient funds in owner account to pay authority rent, marking as expired: %s", name))
|
k.Logger(ctx).Info(fmt.Sprintf("Insufficient funds in owner account to pay authority rent, marking as expired: %s", name))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -699,7 +699,7 @@ func (k Keeper) TryTakeAuthorityRent(ctx sdk.Context, name string, authority typ
|
|||||||
k.SetNameAuthority(ctx, name, &authority)
|
k.SetNameAuthority(ctx, name, &authority)
|
||||||
k.AddBondToAuthorityIndexEntry(ctx, authority.BondId, name)
|
k.AddBondToAuthorityIndexEntry(ctx, authority.BondId, name)
|
||||||
|
|
||||||
ctx.Logger().Info(fmt.Sprintf("Authority rent paid successfully: %s", name))
|
k.Logger(ctx).Info(fmt.Sprintf("Authority rent paid successfully: %s", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListNameAuthorityRecords - get all name authority records.
|
// ListNameAuthorityRecords - get all name authority records.
|
||||||
|
@ -39,14 +39,14 @@ func (k RecordKeeper) OnAuctionWinnerSelected(ctx sdk.Context, auctionID string)
|
|||||||
name := k.GetAuctionToAuthorityMapping(ctx, auctionID)
|
name := k.GetAuctionToAuthorityMapping(ctx, auctionID)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
// We don't know about this auction, ignore.
|
// We don't know about this auction, ignore.
|
||||||
ctx.Logger().Info(fmt.Sprintf("Ignoring auction notification, name mapping not found: %s", auctionID))
|
logger(ctx).Info(fmt.Sprintf("Ignoring auction notification, name mapping not found: %s", auctionID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store := ctx.KVStore(k.storeKey)
|
store := ctx.KVStore(k.storeKey)
|
||||||
if !HasNameAuthority(store, name) {
|
if !HasNameAuthority(store, name) {
|
||||||
// We don't know about this authority, ignore.
|
// We don't know about this authority, ignore.
|
||||||
ctx.Logger().Info(fmt.Sprintf("Ignoring auction notification, authority not found: %s", auctionID))
|
logger(ctx).Info(fmt.Sprintf("Ignoring auction notification, authority not found: %s", auctionID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,12 +71,12 @@ func (k RecordKeeper) OnAuctionWinnerSelected(ctx sdk.Context, auctionID string)
|
|||||||
// Can be used to check if names are older than the authority itself (stale names).
|
// Can be used to check if names are older than the authority itself (stale names).
|
||||||
authority.Height = uint64(ctx.BlockHeight())
|
authority.Height = uint64(ctx.BlockHeight())
|
||||||
|
|
||||||
ctx.Logger().Info(fmt.Sprintf("Winner selected, marking authority as active: %s", name))
|
logger(ctx).Info(fmt.Sprintf("Winner selected, marking authority as active: %s", name))
|
||||||
} else {
|
} else {
|
||||||
// Mark as expired.
|
// Mark as expired.
|
||||||
authority.Status = types.AuthorityExpired
|
authority.Status = types.AuthorityExpired
|
||||||
|
|
||||||
ctx.Logger().Info(fmt.Sprintf("No winner, marking authority as expired: %s", name))
|
logger(ctx).Info(fmt.Sprintf("No winner, marking authority as expired: %s", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
authority.AuctionId = ""
|
authority.AuctionId = ""
|
||||||
@ -85,7 +85,7 @@ func (k RecordKeeper) OnAuctionWinnerSelected(ctx sdk.Context, auctionID string)
|
|||||||
// Forget about this auction now, we no longer need it.
|
// Forget about this auction now, we no longer need it.
|
||||||
removeAuctionToAuthorityMapping(store, auctionID)
|
removeAuctionToAuthorityMapping(store, auctionID)
|
||||||
} else {
|
} else {
|
||||||
ctx.Logger().Info(fmt.Sprintf("Ignoring auction notification, status: %s", auctionObj.Status))
|
logger(ctx).Info(fmt.Sprintf("Ignoring auction notification, status: %s", auctionObj.Status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1375
x/registry/types/query.pb.go
generated
1375
x/registry/types/query.pb.go
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
|
||||||
"github.com/cerc-io/laconicd/x/registry/helpers"
|
"github.com/cerc-io/laconicd/x/registry/helpers"
|
||||||
canonicalJson "github.com/gibson042/canonicaljson-go"
|
"github.com/gibson042/canonicaljson-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -13,10 +13,16 @@ const (
|
|||||||
AuthorityUnderAuction = "auction"
|
AuthorityUnderAuction = "auction"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO if schema records are to be more permissive than allowing a map of fields, this type will
|
||||||
|
// become specific to content records. schema records will either occupy a new message or have new
|
||||||
|
// more general purpose helper types.
|
||||||
|
|
||||||
|
type AttributeMap map[string]interface{}
|
||||||
|
|
||||||
// ReadablePayload represents a signed record payload that can be serialized from/to YAML.
|
// ReadablePayload represents a signed record payload that can be serialized from/to YAML.
|
||||||
type ReadablePayload struct {
|
type ReadablePayload struct {
|
||||||
Record map[string]interface{} `json:"record"`
|
RecordAttributes AttributeMap `json:"record" yaml:"record"`
|
||||||
Signatures []Signature `json:"signatures"`
|
Signatures []Signature `json:"signatures" yaml:"signatures"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadableRecord represents a WNS record.
|
// ReadableRecord represents a WNS record.
|
||||||
@ -28,23 +34,22 @@ type ReadableRecord struct {
|
|||||||
ExpiryTime string `json:"expiryTime,omitempty"`
|
ExpiryTime string `json:"expiryTime,omitempty"`
|
||||||
Deleted bool `json:"deleted,omitempty"`
|
Deleted bool `json:"deleted,omitempty"`
|
||||||
Owners []string `json:"owners,omitempty"`
|
Owners []string `json:"owners,omitempty"`
|
||||||
Attributes map[string]interface{} `json:"attributes,omitempty"`
|
Attributes AttributeMap `json:"attributes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToPayload converts PayloadEncodable to Payload object.
|
// ToPayload converts PayloadEncodable to Payload object.
|
||||||
// Why? Because go-amino can't handle maps: https://github.com/tendermint/go-amino/issues/4.
|
// Why? Because go-amino can't handle maps: https://github.com/tendermint/go-amino/issues/4.
|
||||||
func (payloadObj *ReadablePayload) ToPayload() Payload {
|
func (payloadObj *ReadablePayload) ToPayload() Payload {
|
||||||
// Note: record directly contains the attributes here
|
// Note: record directly contains the attributes here
|
||||||
attributes := helpers.MarshalMapToJSONBytes(payloadObj.Record)
|
attributes := payloadObj.RecordAttributes
|
||||||
payload := Payload{
|
payload := Payload{
|
||||||
Record: &Record{
|
Record: &Record{
|
||||||
Deleted: false,
|
Deleted: false,
|
||||||
Owners: nil,
|
Owners: nil,
|
||||||
Attributes: attributes,
|
Attributes: helpers.MustMarshalJSON(attributes),
|
||||||
},
|
},
|
||||||
Signatures: payloadObj.Signatures,
|
Signatures: payloadObj.Signatures,
|
||||||
}
|
}
|
||||||
// TODO rm error
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,12 +57,28 @@ func (payloadObj *ReadablePayload) ToPayload() Payload {
|
|||||||
func (payload Payload) ToReadablePayload() ReadablePayload {
|
func (payload Payload) ToReadablePayload() ReadablePayload {
|
||||||
var encodable ReadablePayload
|
var encodable ReadablePayload
|
||||||
|
|
||||||
encodable.Record = helpers.UnMarshalMapFromJSONBytes(payload.Record.Attributes)
|
encodable.RecordAttributes = helpers.MustUnmarshalJSON[AttributeMap](payload.Record.Attributes)
|
||||||
encodable.Signatures = payload.Signatures
|
encodable.Signatures = payload.Signatures
|
||||||
|
|
||||||
return encodable
|
return encodable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToRecordObj converts Record to RecordObj.
|
||||||
|
// Why? Because go-amino can't handle maps: https://github.com/tendermint/go-amino/issues/4.
|
||||||
|
func (r *ReadableRecord) ToRecordObj() (Record, error) {
|
||||||
|
var resourceObj Record
|
||||||
|
|
||||||
|
resourceObj.Id = r.ID
|
||||||
|
resourceObj.BondId = r.BondID
|
||||||
|
resourceObj.CreateTime = r.CreateTime
|
||||||
|
resourceObj.ExpiryTime = r.ExpiryTime
|
||||||
|
resourceObj.Deleted = r.Deleted
|
||||||
|
resourceObj.Owners = r.Owners
|
||||||
|
resourceObj.Attributes = helpers.MustMarshalJSON(r.Attributes)
|
||||||
|
|
||||||
|
return resourceObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ToReadableRecord converts Record to a serializable object
|
// ToReadableRecord converts Record to a serializable object
|
||||||
func (r *Record) ToReadableRecord() ReadableRecord {
|
func (r *Record) ToReadableRecord() ReadableRecord {
|
||||||
var resourceObj ReadableRecord
|
var resourceObj ReadableRecord
|
||||||
@ -69,34 +90,16 @@ func (r *Record) ToReadableRecord() ReadableRecord {
|
|||||||
resourceObj.Deleted = r.Deleted
|
resourceObj.Deleted = r.Deleted
|
||||||
resourceObj.Owners = r.Owners
|
resourceObj.Owners = r.Owners
|
||||||
resourceObj.Names = r.Names
|
resourceObj.Names = r.Names
|
||||||
resourceObj.Attributes = helpers.UnMarshalMapFromJSONBytes(r.Attributes)
|
resourceObj.Attributes = helpers.MustUnmarshalJSON[AttributeMap](r.Attributes)
|
||||||
|
|
||||||
return resourceObj
|
return resourceObj
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToRecordObj converts Record to RecordObj.
|
|
||||||
// Why? Because go-amino can't handle maps: https://github.com/tendermint/go-amino/issues/4.
|
|
||||||
func (r *ReadableRecord) ToRecordObj() (Record, error) {
|
|
||||||
attributes := helpers.MarshalMapToJSONBytes(r.Attributes)
|
|
||||||
|
|
||||||
var resourceObj Record
|
|
||||||
|
|
||||||
resourceObj.Id = r.ID
|
|
||||||
resourceObj.BondId = r.BondID
|
|
||||||
resourceObj.CreateTime = r.CreateTime
|
|
||||||
resourceObj.ExpiryTime = r.ExpiryTime
|
|
||||||
resourceObj.Deleted = r.Deleted
|
|
||||||
resourceObj.Owners = r.Owners
|
|
||||||
resourceObj.Attributes = attributes
|
|
||||||
|
|
||||||
return resourceObj, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanonicalJSON returns the canonical JSON representation of the record.
|
// CanonicalJSON returns the canonical JSON representation of the record.
|
||||||
func (r *ReadableRecord) CanonicalJSON() []byte {
|
func (r *ReadableRecord) CanonicalJSON() []byte {
|
||||||
bytes, err := canonicalJson.Marshal(r.Attributes)
|
bytes, err := canonicaljson.Marshal(r.Attributes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("Record marshal error.")
|
panic("error marshaling record: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes
|
return bytes
|
||||||
|
Loading…
Reference in New Issue
Block a user