From 76f6f32c583e960381fea76c778030839c065ef7 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 11 Jun 2024 14:48:47 -0400 Subject: [PATCH 01/79] feat: indexer base types --- indexer/base/README.md | 5 + indexer/base/column.go | 149 +++++++++++++++++ indexer/base/entity.go | 40 +++++ indexer/base/go.mod | 6 + indexer/base/go.sum | 0 indexer/base/kind.go | 304 ++++++++++++++++++++++++++++++++++ indexer/base/listener.go | 114 +++++++++++++ indexer/base/module_schema.go | 8 + indexer/base/table.go | 20 +++ 9 files changed, 646 insertions(+) create mode 100644 indexer/base/README.md create mode 100644 indexer/base/column.go create mode 100644 indexer/base/entity.go create mode 100644 indexer/base/go.mod create mode 100644 indexer/base/go.sum create mode 100644 indexer/base/kind.go create mode 100644 indexer/base/listener.go create mode 100644 indexer/base/module_schema.go create mode 100644 indexer/base/table.go diff --git a/indexer/base/README.md b/indexer/base/README.md new file mode 100644 index 000000000000..0b96a27dc63c --- /dev/null +++ b/indexer/base/README.md @@ -0,0 +1,5 @@ +# Indexer Base + +The indexer base module is designed to provide a stable, zero-dependency base layer for the built-in indexer functionality. Packages that integrate with the indexer should feel free to depend on this package without fear of any external dependencies being pulled in. + +The basic types for specifying index sources, targets and decoders are provided here along with a basic engine that ties these together. A package wishing to be an indexing source could accept an instance of `Engine` directly to be compatible with indexing. A package wishing to be a decoder can use the `Entity` and `Table` types. A package defining an indexing target should implement the `Indexer` interface. \ No newline at end of file diff --git a/indexer/base/column.go b/indexer/base/column.go new file mode 100644 index 000000000000..14fa4e55b322 --- /dev/null +++ b/indexer/base/column.go @@ -0,0 +1,149 @@ +package indexerbase + +import "fmt" + +// Column represents a column in a table schema. +type Column struct { + // Name is the name of the column. + Name string + + // Kind is the basic type of the column. + Kind Kind + + // Nullable indicates whether null values are accepted for the column. + Nullable bool + + // AddressPrefix is the address prefix of the column's kind, currently only used for Bech32AddressKind. + AddressPrefix string + + // EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind. + EnumDefinition EnumDefinition +} + +// EnumDefinition represents the definition of an enum type. +type EnumDefinition struct { + // Name is the name of the enum type. + Name string + + // Values is a list of distinct values that are part of the enum type. + Values []string +} + +// Validate validates the column. +func (c Column) Validate() error { + // non-empty name + if c.Name == "" { + return fmt.Errorf("column name cannot be empty") + } + + // valid kind + if err := c.Kind.Validate(); err != nil { + return fmt.Errorf("invalid column type for %q: %w", c.Name, err) + } + + // address prefix only valid with Bech32AddressKind + if c.Kind == Bech32AddressKind && c.AddressPrefix == "" { + return fmt.Errorf("missing address prefix for column %q", c.Name) + } else if c.Kind != Bech32AddressKind && c.AddressPrefix != "" { + return fmt.Errorf("address prefix is only valid for column %q with type Bech32AddressKind", c.Name) + } + + // enum definition only valid with EnumKind + if c.Kind == EnumKind { + if err := c.EnumDefinition.Validate(); err != nil { + return fmt.Errorf("invalid enum definition for column %q: %w", c.Name, err) + } + } else if c.Kind != EnumKind && c.EnumDefinition.Name != "" && c.EnumDefinition.Values != nil { + return fmt.Errorf("enum definition is only valid for column %q with type EnumKind", c.Name) + } + + return nil +} + +// Validate validates the enum definition. +func (e EnumDefinition) Validate() error { + if e.Name == "" { + return fmt.Errorf("enum definition name cannot be empty") + } + if len(e.Values) == 0 { + return fmt.Errorf("enum definition values cannot be empty") + } + seen := make(map[string]bool, len(e.Values)) + for i, v := range e.Values { + if v == "" { + return fmt.Errorf("enum definition value at index %d cannot be empty for enum %s", i, e.Name) + } + if seen[v] { + return fmt.Errorf("duplicate enum definition value %q for enum %s", v, e.Name) + } + seen[v] = true + } + return nil +} + +// ValidateValue validates that the value conforms to the column's kind and nullability. +func (c Column) ValidateValue(value any) error { + if value == nil { + if !c.Nullable { + return fmt.Errorf("column %q cannot be null", c.Name) + } + return nil + } + return c.Kind.ValidateValue(value) +} + +// ValidateKey validates that the value conforms to the set of columns as a Key in an EntityUpdate. +// See EntityUpdate.Key for documentation on the requirements of such values. +func ValidateKey(cols []Column, value any) error { + if len(cols) == 0 { + return nil + } + + if len(cols) == 1 { + return cols[0].ValidateValue(value) + } + + values, ok := value.([]any) + if !ok { + return fmt.Errorf("expected slice of values for key columns, got %T", value) + } + + if len(cols) != len(values) { + return fmt.Errorf("expected %d key columns, got %d values", len(cols), len(value.([]any))) + } + for i, col := range cols { + if err := col.ValidateValue(values[i]); err != nil { + return fmt.Errorf("invalid value for key column %q: %w", col.Name, err) + } + } + return nil +} + +// ValidateValue validates that the value conforms to the set of columns as a Value in an EntityUpdate. +// See EntityUpdate.Value for documentation on the requirements of such values. +func ValidateValue(cols []Column, value any) error { + valueUpdates, ok := value.(ValueUpdates) + if ok { + colMap := map[string]Column{} + for _, col := range cols { + colMap[col.Name] = col + } + var errs []error + valueUpdates.Iterate(func(colName string, value any) bool { + col, ok := colMap[colName] + if !ok { + errs = append(errs, fmt.Errorf("unknown column %q in value updates", colName)) + } + if err := col.ValidateValue(value); err != nil { + errs = append(errs, fmt.Errorf("invalid value for column %q: %w", colName, err)) + } + return true + }) + if len(errs) > 0 { + return fmt.Errorf("validation errors: %v", errs) + } + return nil + } else { + return ValidateKey(cols, value) + } +} diff --git a/indexer/base/entity.go b/indexer/base/entity.go new file mode 100644 index 000000000000..95f016037fd8 --- /dev/null +++ b/indexer/base/entity.go @@ -0,0 +1,40 @@ +package indexerbase + +// EntityUpdate represents an update operation on an entity in the schema. +type EntityUpdate struct { + // TableName is the name of the table that the entity belongs to in the schema. + TableName string + + // Key returns the value of the primary key of the entity and must conform to these constraints with respect + // that the schema that is defined for the entity: + // - if key represents a single column, then the value must be valid for the first column in that + // column list. For instance, if there is one column in the key of type String, then the value must be of + // type string + // - if key represents multiple columns, then the value must be a slice of values where each value is valid + // for the corresponding column in the column list. For instance, if there are two columns in the key of + // type String, String, then the value must be a slice of two strings. + // If the key has no columns, meaning that this is a singleton entity, then this value is ignored and can be nil. + Key any + + // Value returns the non-primary key columns of the entity and can either conform to the same constraints + // as EntityUpdate.Key or it may be and instance of ValueUpdates. ValueUpdates can be used as a performance + // optimization to avoid copying the values of the entity into the update and/or to omit unchanged columns. + // If this is a delete operation, then this value is ignored and can be nil. + Value any + + // Delete is a flag that indicates whether this update is a delete operation. If true, then the Value field + // is ignored and can be nil. + Delete bool +} + +// ValueUpdates is an interface that represents the value columns of an entity update. Columns that +// were not updated may be excluded from the update. Consumers should be aware that implementations +// may not filter out columns that were unchanged. However, if a column is omitted from the update +// it should be considered unchanged. +type ValueUpdates interface { + + // Iterate iterates over the columns and values in the entity update. The function should return + // true to continue iteration or false to stop iteration. Each column value should conform + // to the requirements of that column's type in the schema. + Iterate(func(col string, value any) bool) +} diff --git a/indexer/base/go.mod b/indexer/base/go.mod new file mode 100644 index 000000000000..c369648761e8 --- /dev/null +++ b/indexer/base/go.mod @@ -0,0 +1,6 @@ +module cosmossdk.io/indexer/base + +// NOTE: this go.mod should have zero dependencies and remain on an older version of Go +// to be compatible with legacy codebases. + +go 1.19 diff --git a/indexer/base/go.sum b/indexer/base/go.sum new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/indexer/base/kind.go b/indexer/base/kind.go new file mode 100644 index 000000000000..d1873944fb1b --- /dev/null +++ b/indexer/base/kind.go @@ -0,0 +1,304 @@ +package indexerbase + +import ( + "encoding/json" + "fmt" + "time" +) + +// Kind represents the basic type of a column in the table schema. +// Each kind defines the types of go values which should be accepted +// by listeners and generated by decoders when providing entity updates. +type Kind int + +const ( + // InvalidKind indicates that an invalid type. + InvalidKind Kind = iota + + // StringKind is a string type and values of this type must be of the go type string + // or implement fmt.Stringer(). + StringKind + + // BytesKind is a bytes type and values of this type must be of the go type []byte. + BytesKind + + // Int8Kind is an int8 type and values of this type must be of the go type int8. + Int8Kind + + // Uint8Kind is a uint8 type and values of this type must be of the go type uint8. + Uint8Kind + + // Int16Kind is an int16 type and values of this type must be of the go type int16. + Int16Kind + + // Uint16Kind is a uint16 type and values of this type must be of the go type uint16. + Uint16Kind + + // Int32Kind is an int32 type and values of this type must be of the go type int32. + Int32Kind + + // Uint32Kind is a uint32 type and values of this type must be of the go type uint32. + Uint32Kind + + // Int64Kind is an int64 type and values of this type must be of the go type int64. + Int64Kind + + // Uint64Kind is a uint64 type and values of this type must be of the go type uint64. + Uint64Kind + + // IntegerKind represents an arbitrary precision integer number. Values of this type must + // be of the go type string or a type that implements fmt.Stringer with the resulted string + // formatted as an integer number. + IntegerKind + + // DecimalKind represents an arbitrary precision decimal or integer number. Values of this type + // must be of the go type string or a type that implements fmt.Stringer with the resulting string + // formatted as decimal numbers with an optional fractional part. Exponential E-notation + // is supported but NaN and Infinity are not. + DecimalKind + + // BoolKind is a boolean type and values of this type must be of the go type bool. + BoolKind + + // TimeKind is a time type and values of this type must be of the go type time.Time. + TimeKind + + // DurationKind is a duration type and values of this type must be of the go type time.Duration. + DurationKind + + // Float32Kind is a float32 type and values of this type must be of the go type float32. + Float32Kind + + // Float64Kind is a float64 type and values of this type must be of the go type float64. + Float64Kind + + // Bech32AddressKind is a bech32 address type and values of this type must be of the go type string or []byte + // or a type which implements fmt.Stringer. Columns of this type are expected to set the AddressPrefix field + // in the column definition to the bech32 address prefix. + Bech32AddressKind + + // EnumKind is an enum type and values of this type must be of the go type string or implement fmt.Stringer. + // Columns of this type are expected to set the EnumDefinition field in the column definition to the enum + // definition. + EnumKind + + // JSONKind is a JSON type and values of this type can either be of go type json.RawMessage + // or any type that can be marshaled to JSON using json.Marshal. + JSONKind +) + +// Validate returns an error if the kind is invalid. +func (t Kind) Validate() error { + if t <= InvalidKind { + return fmt.Errorf("unknown type: %d", t) + } + if t > JSONKind { + return fmt.Errorf("invalid type: %d", t) + } + return nil +} + +// ValidateValue returns an error if the value does not the type go type specified by the kind. +// Some columns may accept nil values, however, this method does not have any notion of +// nullability. It only checks that the value is of the correct type. +func (t Kind) ValidateValue(value any) error { + switch t { + case StringKind: + _, ok := value.(string) + _, ok2 := value.(fmt.Stringer) + if !ok && !ok2 { + return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + } + case BytesKind: + _, ok := value.([]byte) + if !ok { + return fmt.Errorf("expected []byte, got %T", value) + } + case Int8Kind: + _, ok := value.(int8) + if !ok { + return fmt.Errorf("expected int8, got %T", value) + } + case Uint8Kind: + _, ok := value.(uint8) + if !ok { + return fmt.Errorf("expected uint8, got %T", value) + } + case Int16Kind: + _, ok := value.(int16) + if !ok { + return fmt.Errorf("expected int16, got %T", value) + } + case Uint16Kind: + _, ok := value.(uint16) + if !ok { + return fmt.Errorf("expected uint16, got %T", value) + } + case Int32Kind: + _, ok := value.(int32) + if !ok { + return fmt.Errorf("expected int32, got %T", value) + } + case Uint32Kind: + _, ok := value.(uint32) + if !ok { + return fmt.Errorf("expected uint32, got %T", value) + } + case Int64Kind: + _, ok := value.(int64) + if !ok { + return fmt.Errorf("expected int64, got %T", value) + } + case Uint64Kind: + _, ok := value.(uint64) + if !ok { + return fmt.Errorf("expected uint64, got %T", value) + } + case IntegerKind: + _, ok := value.(string) + _, ok2 := value.(fmt.Stringer) + if !ok && !ok2 { + return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + } + case DecimalKind: + _, ok := value.(string) + _, ok2 := value.(fmt.Stringer) + if !ok && !ok2 { + return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + } + case BoolKind: + _, ok := value.(bool) + if !ok { + return fmt.Errorf("expected bool, got %T", value) + } + case TimeKind: + _, ok := value.(time.Time) + if !ok { + return fmt.Errorf("expected time.Time, got %T", value) + } + case DurationKind: + _, ok := value.(time.Duration) + if !ok { + return fmt.Errorf("expected time.Duration, got %T", value) + } + case Float32Kind: + _, ok := value.(float32) + if !ok { + return fmt.Errorf("expected float32, got %T", value) + } + case Float64Kind: + _, ok := value.(float64) + if !ok { + return fmt.Errorf("expected float64, got %T", value) + } + case Bech32AddressKind: + _, ok := value.(string) + _, ok2 := value.([]byte) + _, ok3 := value.(fmt.Stringer) + if !ok && !ok2 && !ok3 { + return fmt.Errorf("expected string or []byte, got %T", value) + } + case EnumKind: + _, ok := value.(string) + _, ok2 := value.(fmt.Stringer) + if !ok && !ok2 { + return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + } + case JSONKind: + return nil + default: + return fmt.Errorf("invalid type: %d", t) + } + return nil +} + +// String returns a string representation of the kind. +func (t Kind) String() string { + switch t { + case StringKind: + return "string" + case BytesKind: + return "bytes" + case Int8Kind: + return "int8" + case Uint8Kind: + return "uint8" + case Int16Kind: + return "int16" + case Uint16Kind: + return "uint16" + case Int32Kind: + return "int32" + case Uint32Kind: + return "uint32" + case Int64Kind: + return "int64" + case Uint64Kind: + return "uint64" + case DecimalKind: + return "decimal" + case IntegerKind: + return "integer" + case BoolKind: + return "bool" + case TimeKind: + return "time" + case DurationKind: + return "duration" + case Float32Kind: + return "float32" + case Float64Kind: + return "float64" + case Bech32AddressKind: + return "bech32address" + case EnumKind: + return "enum" + case JSONKind: + return "json" + default: + return "" + } +} + +// KindForGoValue finds the simplest kind that can represent the given go value. It will not, however, +// return kinds such as IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind which all can be +// represented as strings. It will return InvalidKind if the value is not a simple type. +func KindForGoValue(value any) Kind { + switch value.(type) { + case string, fmt.Stringer: + return StringKind + case []byte: + return BytesKind + case int8: + return Int8Kind + case uint8: + return Uint8Kind + case int16: + return Int16Kind + case uint16: + return Uint16Kind + case int32: + return Int32Kind + case uint32: + return Uint32Kind + case int64: + return Int64Kind + case uint64: + return Uint64Kind + case float32: + return Float32Kind + case float64: + return Float64Kind + case bool: + return BoolKind + case time.Time: + return TimeKind + case time.Duration: + return DurationKind + case json.RawMessage: + return JSONKind + default: + } + + return InvalidKind +} diff --git a/indexer/base/listener.go b/indexer/base/listener.go new file mode 100644 index 000000000000..cccd6f08e153 --- /dev/null +++ b/indexer/base/listener.go @@ -0,0 +1,114 @@ +package indexerbase + +import ( + "encoding/json" +) + +// Listener is an interface that defines methods for listening to both raw and logical blockchain data. +// It is valid for any of the methods to be nil, in which case the listener will not be called for that event. +// Listeners should understand the guarantees that are provided by the source they are listening to and +// understand which methods will or will not be called. For instance, most blockchains will not do logical +// decoding of data out of the box, so the EnsureLogicalSetup and OnEntityUpdate methods will not be called. +// These methods will only be called when listening logical decoding is setup. +type Listener struct { + // StartBlock is called at the beginning of processing a block. + StartBlock func(uint64) error + + // OnBlockHeader is called when a block header is received. + OnBlockHeader func(BlockHeaderData) error + + // OnTx is called when a transaction is received. + OnTx func(TxData) error + + // OnEvent is called when an event is received. + OnEvent func(EventData) error + + // OnKVPair is called when a key-value has been written to the store for a given module. + OnKVPair func(module string, key, value []byte, delete bool) error + + // Commit is called when state is commited, usually at the end of a block. Any + // indexers should commit their data when this is called and return an error if + // they are unable to commit. + Commit func() error + + // EnsureLogicalSetup should be called whenever the blockchain process starts OR whenever + // logical decoding of a module is initiated. An indexer listening to this event + // should ensure that they have performed whatever initialization steps (such as database + // migrations) required to receive OnEntityUpdate events for the given module. If the + // schema is incompatible with the existing schema, the listener should return an error. + // If the listener is persisting state for the module, it should return the last block + // that was saved for the module so that the framework can determine whether it is safe + // to resume indexing from the current height or whether there is a gap (usually an error). + // If the listener does not persist any state for the module, it should return 0 for lastBlock + // and nil for error. + // If the listener has initialized properly and would like to persist state for the module, + // but does not have any persisted state yet, it should return -1 for lastBlock and nil for error. + // In this case, the framework will perform a "catch-up sync" calling OnEntityUpdate for every + // entity already in the module followed by CommitCatchupSync before processing new block data. + EnsureLogicalSetup func(module string, schema ModuleSchema) (lastBlock int64, err error) + + // OnEntityUpdate is called whenever an entity is updated in the module. This is only called + // when logical data is available. It should be assumed that the same data in raw form + // is also passed to OnKVPair. + OnEntityUpdate func(module string, update EntityUpdate) error + + // CommitCatchupSync is called after all existing entities for a module have been passed to + // OnEntityUpdate during a catch-up sync which has been initiated by return -1 for lastBlock + // in EnsureLogicalSetup. The listener should commit all the data that has been received at + // this point and also save the block number as the last block that has been processed so + // that processing of regular block data can resume from this point in the future. + CommitCatchupSync func(module string, block uint64) error +} + +// BlockHeaderData represents the raw block header data that is passed to a listener. +type BlockHeaderData struct { + // Height is the height of the block. + Height uint64 + + // Bytes is the raw byte representation of the block header. + Bytes ToBytes + + // JSON is the JSON representation of the block header. It should generally be a JSON object. + JSON ToJSON +} + +// TxData represents the raw transaction data that is passed to a listener. +type TxData struct { + // TxIndex is the index of the transaction in the block. + TxIndex int32 + + // Bytes is the raw byte representation of the transaction. + Bytes ToBytes + + // JSON is the JSON representation of the transaction. It should generally be a JSON object. + JSON ToJSON +} + +// EventData represents event data that is passed to a listener. +type EventData struct { + // TxIndex is the index of the transaction in the block to which this event is associated. + // It should be set to a negative number if the event is not associated with a transaction. + // Canonically -1 should be used to represent begin block processing and -2 should be used to + // represent end block processing. + TxIndex int32 + + // MsgIndex is the index of the message in the transaction to which this event is associated. + // If TxIndex is negative, this index could correspond to the index of the message in + // begin or end block processing if such indexes exist, or it can be set to zero. + MsgIndex uint32 + + // EventIndex is the index of the event in the message to which this event is associated. + EventIndex uint32 + + // Type is the type of the event. + Type string + + // Data is the JSON representation of the event data. It should generally be a JSON object. + Data ToJSON +} + +// ToBytes is a function that lazily returns the raw byte representation of data. +type ToBytes = func() ([]byte, error) + +// ToJSON is a function that lazily returns the JSON representation of data. +type ToJSON = func() (json.RawMessage, error) diff --git a/indexer/base/module_schema.go b/indexer/base/module_schema.go new file mode 100644 index 000000000000..4e8b81c2be3c --- /dev/null +++ b/indexer/base/module_schema.go @@ -0,0 +1,8 @@ +package indexerbase + +// ModuleSchema represents the logical schema of a module for purposes of indexing and querying. +type ModuleSchema struct { + + // Tables is a list of tables that are part of the schema for the module. + Tables []Table +} diff --git a/indexer/base/table.go b/indexer/base/table.go new file mode 100644 index 000000000000..2d076f6d3eb8 --- /dev/null +++ b/indexer/base/table.go @@ -0,0 +1,20 @@ +package indexerbase + +// Table represents a table in the schema of a module. +type Table struct { + // Name is the name of the table. + Name string + + // KeyColumns is a list of columns that make up the primary key of the table. + KeyColumns []Column + + // ValueColumns is a list of columns that are not part of the primary key of the table. + ValueColumns []Column + + // RetainDeletions is a flag that indicates whether the indexer should retain + // deleted rows in the database and flag them as deleted rather than actually + // deleting the row. For many types of data in state, the data is deleted even + // though it is still valid in order to save space. Indexers will want to have + // the option of retaining such data and distinguishing from other "true" deletions. + RetainDeletions bool +} From 63aeb85ecec56fd4cda2f21852ab5e9fde0b6770 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 11 Jun 2024 15:11:07 -0400 Subject: [PATCH 02/79] WIP on tests --- indexer/base/column.go | 4 +- indexer/base/column_test.go | 7 + indexer/base/kind.go | 19 ++- indexer/base/kind_test.go | 290 ++++++++++++++++++++++++++++++++++++ 4 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 indexer/base/column_test.go create mode 100644 indexer/base/kind_test.go diff --git a/indexer/base/column.go b/indexer/base/column.go index 14fa4e55b322..30b75d161c82 100644 --- a/indexer/base/column.go +++ b/indexer/base/column.go @@ -82,6 +82,8 @@ func (e EnumDefinition) Validate() error { } // ValidateValue validates that the value conforms to the column's kind and nullability. +// It currently does not do any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind +// values are valid for their respective types behind conforming to the correct go type. func (c Column) ValidateValue(value any) error { if value == nil { if !c.Nullable { @@ -89,7 +91,7 @@ func (c Column) ValidateValue(value any) error { } return nil } - return c.Kind.ValidateValue(value) + return c.Kind.ValidateValueType(value) } // ValidateKey validates that the value conforms to the set of columns as a Key in an EntityUpdate. diff --git a/indexer/base/column_test.go b/indexer/base/column_test.go new file mode 100644 index 000000000000..b646247b058c --- /dev/null +++ b/indexer/base/column_test.go @@ -0,0 +1,7 @@ +package indexerbase + +import "testing" + +func TestColumnValidate(t *testing.T) { + +} diff --git a/indexer/base/kind.go b/indexer/base/kind.go index d1873944fb1b..0c86ab90f4c1 100644 --- a/indexer/base/kind.go +++ b/indexer/base/kind.go @@ -47,7 +47,7 @@ const ( Uint64Kind // IntegerKind represents an arbitrary precision integer number. Values of this type must - // be of the go type string or a type that implements fmt.Stringer with the resulted string + // be of the go type int64, string or a type that implements fmt.Stringer with the resulted string // formatted as an integer number. IntegerKind @@ -98,10 +98,12 @@ func (t Kind) Validate() error { return nil } -// ValidateValue returns an error if the value does not the type go type specified by the kind. +// ValidateValueType returns an error if the value does not the type go type specified by the kind. // Some columns may accept nil values, however, this method does not have any notion of // nullability. It only checks that the value is of the correct type. -func (t Kind) ValidateValue(value any) error { +// It also doesn't perform any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind +// values are valid for their respective types. +func (t Kind) ValidateValueType(value any) error { switch t { case StringKind: _, ok := value.(string) @@ -157,7 +159,8 @@ func (t Kind) ValidateValue(value any) error { case IntegerKind: _, ok := value.(string) _, ok2 := value.(fmt.Stringer) - if !ok && !ok2 { + _, ok3 := value.(int64) + if !ok && !ok2 && !ok3 { return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) } case DecimalKind: @@ -262,7 +265,10 @@ func (t Kind) String() string { // KindForGoValue finds the simplest kind that can represent the given go value. It will not, however, // return kinds such as IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind which all can be -// represented as strings. It will return InvalidKind if the value is not a simple type. +// represented as strings. Generally all values which do not have a more specific type will +// return JSONKind because the framework cannot decide at this point whether the value +// can or cannot be marshaled to JSON. This method should generally only be used as a fallback +// when the kind of a column is not specified more specifically. func KindForGoValue(value any) Kind { switch value.(type) { case string, fmt.Stringer: @@ -298,7 +304,6 @@ func KindForGoValue(value any) Kind { case json.RawMessage: return JSONKind default: + return JSONKind } - - return InvalidKind } diff --git a/indexer/base/kind_test.go b/indexer/base/kind_test.go new file mode 100644 index 000000000000..18075535b067 --- /dev/null +++ b/indexer/base/kind_test.go @@ -0,0 +1,290 @@ +package indexerbase + +import ( + "strings" + "testing" + "time" +) + +func TestKind_Validate(t *testing.T) { + validKinds := []Kind{ + StringKind, + BytesKind, + Int8Kind, + Uint8Kind, + Int16Kind, + Uint16Kind, + Int32Kind, + Uint32Kind, + Int64Kind, + Uint64Kind, + IntegerKind, + DecimalKind, + BoolKind, + EnumKind, + Bech32AddressKind, + } + + for _, kind := range validKinds { + if err := kind.Validate(); err != nil { + t.Errorf("expected valid kind %s to pass validation, got: %v", kind, err) + } + } + + invalidKinds := []Kind{ + Kind(-1), + InvalidKind, + Kind(100), + } + + for _, kind := range invalidKinds { + if err := kind.Validate(); err == nil { + t.Errorf("expected invalid kind %s to fail validation, got: %v", kind, err) + } + } +} + +func TestKind_ValidateValue(t *testing.T) { + tests := []struct { + kind Kind + value any + valid bool + }{ + { + kind: StringKind, + value: "hello", + valid: true, + }, + { + kind: StringKind, + value: &strings.Builder{}, + valid: true, + }, + { + kind: StringKind, + value: []byte("hello"), + valid: false, + }, + { + kind: BytesKind, + value: []byte("hello"), + valid: true, + }, + { + kind: BytesKind, + value: "hello", + valid: false, + }, + { + kind: Int8Kind, + value: int8(1), + valid: true, + }, + { + kind: Int8Kind, + value: int16(1), + valid: false, + }, + { + kind: Uint8Kind, + value: uint8(1), + valid: true, + }, + { + kind: Uint8Kind, + value: uint16(1), + valid: false, + }, + { + kind: Int16Kind, + value: int16(1), + valid: true, + }, + { + kind: Int16Kind, + value: int32(1), + valid: false, + }, + { + kind: Uint16Kind, + value: uint16(1), + valid: true, + }, + { + kind: Uint16Kind, + value: uint32(1), + valid: false, + }, + { + kind: Int32Kind, + value: int32(1), + valid: true, + }, + { + kind: Int32Kind, + value: int64(1), + valid: false, + }, + { + kind: Uint32Kind, + value: uint32(1), + valid: true, + }, + { + kind: Uint32Kind, + value: uint64(1), + valid: false, + }, + { + kind: Int64Kind, + value: int64(1), + valid: true, + }, + { + kind: Int64Kind, + value: int32(1), + valid: false, + }, + { + kind: Uint64Kind, + value: uint64(1), + valid: true, + }, + { + kind: Uint64Kind, + value: uint32(1), + valid: false, + }, + { + kind: IntegerKind, + value: "1", + valid: true, + }, + //{ + // kind: IntegerKind, + // value: (&strings.Builder{}).WriteString("1"), + // valid: true, + //}, + { + kind: IntegerKind, + value: int32(1), + valid: false, + }, + { + kind: IntegerKind, + value: int64(1), + valid: true, + }, + { + kind: DecimalKind, + value: "1.0", + valid: true, + }, + { + kind: DecimalKind, + value: "1", + valid: true, + }, + { + kind: DecimalKind, + value: "1.1e4", + valid: true, + }, + //{ + // kind: DecimalKind, + // value: (&strings.Builder{}).WriteString("1.0"), + // valid: true, + //}, + { + kind: DecimalKind, + value: int32(1), + valid: false, + }, + { + kind: Bech32AddressKind, + value: "cosmos1hsk6jryyqjfhp5g7c0nh4n6dd45ygctnxglp5h", + valid: true, + }, + //{ + // kind: Bech32AddressKind, + // value: (&strings.Builder{}).WriteString("cosmos1hsk6jryyqjfhp5g7c0nh4n6dd45ygctnxglp5h"), + // valid: true, + //}, + { + kind: Bech32AddressKind, + value: 1, + valid: false, + }, + { + kind: BoolKind, + value: true, + valid: true, + }, + { + kind: BoolKind, + value: false, + valid: true, + }, + { + kind: BoolKind, + value: 1, + valid: false, + }, + { + kind: EnumKind, + value: "hello", + valid: true, + }, + //{ + // kind: EnumKind, + // value: (&strings.Builder{}).WriteString("hello"), + // valid: true, + //}, + { + kind: EnumKind, + value: 1, + valid: false, + }, + { + kind: TimeKind, + value: time.Now(), + valid: true, + }, + { + kind: TimeKind, + value: "hello", + valid: false, + }, + { + kind: DurationKind, + value: time.Second, + valid: true, + }, + { + kind: DurationKind, + value: "hello", + valid: false, + }, + { + kind: Float32Kind, + value: float32(1.0), + valid: true, + }, + { + kind: Float32Kind, + value: float64(1.0), + valid: false, + }, + // TODO float64, json + } + + for i, tt := range tests { + err := tt.kind.ValidateValueType(tt.value) + if tt.valid && err != nil { + t.Errorf("test %d: expected valid value %v for kind %s to pass validation, got: %v", i, tt.value, tt.kind, err) + } + if !tt.valid && err == nil { + t.Errorf("test %d: expected invalid value %v for kind %s to fail validation, got: %v", i, tt.value, tt.kind, err) + } + } +} From 216e8f87298784fe65a0840ffa6a8743dad4cb31 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 12 Jun 2024 12:20:15 -0400 Subject: [PATCH 03/79] update listener --- indexer/base/listener.go | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/indexer/base/listener.go b/indexer/base/listener.go index cccd6f08e153..05b8de2a5013 100644 --- a/indexer/base/listener.go +++ b/indexer/base/listener.go @@ -8,9 +8,15 @@ import ( // It is valid for any of the methods to be nil, in which case the listener will not be called for that event. // Listeners should understand the guarantees that are provided by the source they are listening to and // understand which methods will or will not be called. For instance, most blockchains will not do logical -// decoding of data out of the box, so the EnsureLogicalSetup and OnEntityUpdate methods will not be called. +// decoding of data out of the box, so the InitializeModuleSchema and OnEntityUpdate methods will not be called. // These methods will only be called when listening logical decoding is setup. type Listener struct { + // Initialize is called when the listener is initialized before any other methods are called. + // The lastBlock return value should be the last block height the listener persisted if it is + // persisting block data, 0 if it is not interested in persisting block data, or -1 if it is + // persisting block data but has not persisted any data yet. + Initialize func(InitializationData) (lastBlock int64, err error) + // StartBlock is called at the beginning of processing a block. StartBlock func(uint64) error @@ -24,18 +30,19 @@ type Listener struct { OnEvent func(EventData) error // OnKVPair is called when a key-value has been written to the store for a given module. - OnKVPair func(module string, key, value []byte, delete bool) error + OnKVPair func(moduleName string, key, value []byte, delete bool) error // Commit is called when state is commited, usually at the end of a block. Any // indexers should commit their data when this is called and return an error if // they are unable to commit. Commit func() error - // EnsureLogicalSetup should be called whenever the blockchain process starts OR whenever + // InitializeModuleSchema should be called whenever the blockchain process starts OR whenever // logical decoding of a module is initiated. An indexer listening to this event // should ensure that they have performed whatever initialization steps (such as database // migrations) required to receive OnEntityUpdate events for the given module. If the - // schema is incompatible with the existing schema, the listener should return an error. + // indexer's schema is incompatible with the module's on-chain schema, the listener should return + // an error. // If the listener is persisting state for the module, it should return the last block // that was saved for the module so that the framework can determine whether it is safe // to resume indexing from the current height or whether there is a gap (usually an error). @@ -45,7 +52,7 @@ type Listener struct { // but does not have any persisted state yet, it should return -1 for lastBlock and nil for error. // In this case, the framework will perform a "catch-up sync" calling OnEntityUpdate for every // entity already in the module followed by CommitCatchupSync before processing new block data. - EnsureLogicalSetup func(module string, schema ModuleSchema) (lastBlock int64, err error) + InitializeModuleSchema func(module string, schema ModuleSchema) (lastBlock int64, err error) // OnEntityUpdate is called whenever an entity is updated in the module. This is only called // when logical data is available. It should be assumed that the same data in raw form @@ -54,10 +61,29 @@ type Listener struct { // CommitCatchupSync is called after all existing entities for a module have been passed to // OnEntityUpdate during a catch-up sync which has been initiated by return -1 for lastBlock - // in EnsureLogicalSetup. The listener should commit all the data that has been received at + // in InitializeModuleSchema. The listener should commit all the data that has been received at // this point and also save the block number as the last block that has been processed so // that processing of regular block data can resume from this point in the future. CommitCatchupSync func(module string, block uint64) error + + // SubscribedModules is a map of modules that the listener is interested in receiving events for in OnKVPair and + // logical decoding listeners (if these are registered). If this is left nil but listeners are registered, + // it is assumed that the listener is interested in all modules. + SubscribedModules map[string]bool +} + +// InitializationData represents initialization data that is passed to a listener. +type InitializationData struct { + + // HasEventAlignedWrites indicates that the blockchain data source will emit KV-pair events + // in an order aligned with transaction, message and event callbacks. If this is true + // then indexers can assume that KV-pair data is associated with these specific transactions, messages + // and events. This may be useful for indexers which store a log of all operations (such as immutable + // or version controlled databases) so that the history log can include fine grain correlation between + // state updates and transactions, messages and events. If this value is false, then indexers should + // assume that KV-pair data occurs out of order with respect to transaction, message and event callbacks - + // the only safe assumption being that KV-pair data is associated with the block in which it was emitted. + HasEventAlignedWrites bool } // BlockHeaderData represents the raw block header data that is passed to a listener. From b65f3cd9d85cfae228a87fbbd9bdfe1b1491afd3 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 12 Jun 2024 12:57:28 -0400 Subject: [PATCH 04/79] feat(indexer): add listener test fixture --- indexer/base/table.go | 3 ++ indexer/base/testing/testing.go | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 indexer/base/testing/testing.go diff --git a/indexer/base/table.go b/indexer/base/table.go index 2d076f6d3eb8..1f192724b4e8 100644 --- a/indexer/base/table.go +++ b/indexer/base/table.go @@ -6,9 +6,12 @@ type Table struct { Name string // KeyColumns is a list of columns that make up the primary key of the table. + // It can be empty in which case indexers should assume that this table is + // a singleton and ony has one value. KeyColumns []Column // ValueColumns is a list of columns that are not part of the primary key of the table. + // It can be empty in the case where all columns are part of the primary key. ValueColumns []Column // RetainDeletions is a flag that indicates whether the indexer should retain diff --git a/indexer/base/testing/testing.go b/indexer/base/testing/testing.go new file mode 100644 index 000000000000..f44a4cf0e620 --- /dev/null +++ b/indexer/base/testing/testing.go @@ -0,0 +1,63 @@ +package indexertesting + +import indexerbase "cosmossdk.io/indexer/base" + +// ListenerTestFixture is a test fixture for testing listener implementations with a pre-defined data set +// that attempts to cover all known types of tables and columns. The test data currently includes data for +// two fake modules over three blocks of data. The data set should remain relatively stable between releases +// and generally only be changed when new features are added, so it should be suitable for regression or golden tests. +type ListenerTestFixture struct { + listener indexerbase.Listener +} + +type ListenerTestFixtureOptions struct { + EventAlignedWrites bool +} + +func NewListenerTestFixture(listener indexerbase.Listener, options ListenerTestFixtureOptions) *ListenerTestFixture { + return &ListenerTestFixture{ + listener: listener, + } +} + +func (f *ListenerTestFixture) Initialize() error { + return nil +} + +func (f *ListenerTestFixture) NextBlock() (bool, error) { + return false, nil +} + +func (f *ListenerTestFixture) block1() error { + return nil +} + +func (f *ListenerTestFixture) block2() error { + return nil +} + +func (f *ListenerTestFixture) block3() error { + return nil +} + +var moduleSchemaA = indexerbase.ModuleSchema{ + Tables: []indexerbase.Table{ + { + "A1", + []indexerbase.Column{}, + []indexerbase.Column{}, + false, + }, + }, +} + +var moduleSchemaB = indexerbase.ModuleSchema{ + Tables: []indexerbase.Table{ + { + "B1", + []indexerbase.Column{}, + []indexerbase.Column{}, + false, + }, + }, +} From d8b683177e330d3f4e2af1087aa686237fa43fd3 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 12 Jun 2024 13:43:39 -0400 Subject: [PATCH 05/79] feat(indexer): postgres create table --- indexer/postgres/create_table.go | 110 ++++++++++++++++++++++++++++ indexer/postgres/go.mod | 17 +++++ indexer/postgres/go.sum | 12 ++++ indexer/postgres/indexer.go | 118 +++++++++++++++++++++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 indexer/postgres/create_table.go create mode 100644 indexer/postgres/go.mod create mode 100644 indexer/postgres/go.sum create mode 100644 indexer/postgres/indexer.go diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go new file mode 100644 index 000000000000..1cb15b9bf472 --- /dev/null +++ b/indexer/postgres/create_table.go @@ -0,0 +1,110 @@ +package postgres + +import ( + "bytes" + "fmt" + "io" + "strings" + + indexerbase "cosmossdk.io/indexer/base" +) + +func (i *indexer) createTableStatement(tableSchema indexerbase.Table) (string, error) { + w := &bytes.Buffer{} + _, err := fmt.Fprintf(w, "CREATE TABLE IF NOT EXISTS %s (\n\t", tableSchema.Name) + if err != nil { + return "", err + } + + isSingleton := false + if len(tableSchema.KeyColumns) == 0 { + isSingleton = true + _, err = fmt.Fprintf(w, "_id INTEGER NOT NULL CHECK (_id = 1),\n\t") + } else { + for _, col := range tableSchema.KeyColumns { + err = i.createColumnDef(w, col) + if err != nil { + return "", err + } + } + } + + for _, col := range tableSchema.ValueColumns { + err = i.createColumnDef(w, col) + if err != nil { + return "", err + } + } + + var pKeys []string + if !isSingleton { + for _, col := range tableSchema.KeyColumns { + pKeys = append(pKeys, col.Name) + } + } else { + pKeys = []string{"_id"} + } + + _, err = fmt.Fprintf(w, "PRIMARY KEY (%s)\n", strings.Join(pKeys, ", ")) + if err != nil { + return "", err + } + + _, err = fmt.Fprintf(w, ");") + if err != nil { + return "", err + } + + return w.String(), nil +} + +func (i *indexer) createColumnDef(w io.Writer, col indexerbase.Column) error { + typeStr, err := i.colType(col) + if err != nil { + return err + } + + _, err = fmt.Fprintf(w, "%s %s,\n\t", col.Name, typeStr) + return err +} + +func (i *indexer) colType(col indexerbase.Column) (string, error) { + switch col.Type { + case indexerbase.StringKind: + return "TEXT", nil + case indexerbase.BoolKind: + return "BOOLEAN", nil + case indexerbase.BytesKind: + return "BYTEA", nil + case indexerbase.Int8Kind: + return "SMALLINT", nil + case indexerbase.Int16Kind: + return "SMALLINT", nil + case indexerbase.Int32Kind: + return "INTEGER", nil + case indexerbase.Int64Kind: + return "BIGINT", nil + case indexerbase.Uint8Kind: + return "SMALLINT", nil + case indexerbase.Uint16Kind: + return "INTEGER", nil + case indexerbase.Uint32Kind: + return "BIGINT", nil + case indexerbase.Uint64Kind: + return "NUMERIC", nil + case indexerbase.DecimalKind: + return "NUMERIC", nil + case indexerbase.Float32Kind: + return "REAL", nil + case indexerbase.Float64Kind: + return "DOUBLE PRECISION", nil + case indexerbase.EnumKind: + return "TEXT", fmt.Errorf("enums not supported yet") + case indexerbase.JSONKind: + return "JSONB", nil + case indexerbase.Bech32AddressKind: + return "TEXT", nil + default: + return "", fmt.Errorf("unsupported type %v", col.Type) + } +} diff --git a/indexer/postgres/go.mod b/indexer/postgres/go.mod new file mode 100644 index 000000000000..2bfab0f3010d --- /dev/null +++ b/indexer/postgres/go.mod @@ -0,0 +1,17 @@ +module cosmossdk.io/indexer/postgres + +require ( + cosmossdk.io/indexer/base v0.0.0-00010101000000-000000000000 + github.com/jackc/pgx/v5 v5.6.0 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) + +go 1.22 + +replace cosmossdk.io/indexer/base => ../base diff --git a/indexer/postgres/go.sum b/indexer/postgres/go.sum new file mode 100644 index 000000000000..62fbcdb3a541 --- /dev/null +++ b/indexer/postgres/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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= diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go new file mode 100644 index 000000000000..c8bd900d26a9 --- /dev/null +++ b/indexer/postgres/indexer.go @@ -0,0 +1,118 @@ +package postgres + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" + + indexerbase "cosmossdk.io/indexer/base" +) + +type indexer struct { + ctx context.Context + conn *pgx.Conn + tables map[string]*tableInfo +} + +type Options struct{} + +func NewIndexer(ctx context.Context, opts Options) (indexerbase.Listener, error) { + // get env var DATABASE_URL + dbUrl, ok := os.LookupEnv("DATABASE_URL") + if !ok { + panic("DATABASE_URL not set") + } + + conn, err := pgx.Connect(ctx, dbUrl) + if err != nil { + panic(err) + } + + i := &indexer{ + ctx: ctx, + conn: conn, + tables: map[string]*tableInfo{}, + } + return i.logicalListener() +} + +func (i *indexer) logicalListener() (indexerbase.Listener, error) { + return indexerbase.Listener{ + StartBlock: i.startBlock, + Commit: i.commit, + //EnsureSetup: i.ensureSetup, + //OnEntityUpdate: i.onEntityUpdate, + }, nil +} + +func (i *indexer) ensureSetup(data indexerbase.LogicalSetupData) error { + for _, table := range data.Schema.Tables { + createTable, err := i.createTableStatement(table) + if err != nil { + return err + } + fmt.Printf("%s\n", createTable) + _, err = i.conn.Exec(context.Background(), createTable) + if err != nil { + return err + } + + _, err = i.conn.Exec(context.Background(), fmt.Sprintf("GRANT SELECT ON %s TO public;", table.Name)) + if err != nil { + return err + } + + i.tables[table.Name] = &tableInfo{table: table} + } + return nil +} + +type tableInfo struct { + table indexerbase.Table +} + +func (i *indexer) startBlock(u uint64) error { + return nil +} + +// func (i indexer) IndexBlockHeader(data *indexerbase.BlockHeaderData) error { +// //TODO implement me +// panic("implement me") +// } +// +// func (i indexer) IndexTx(data *indexerbase.TxData) error { +// //TODO implement me +// panic("implement me") +// } +// +// func (i indexer) IndexEvent(data *indexerbase.EventData) error { +// //TODO implement me +// panic("implement me") +// } + +func (i *indexer) onEntityUpdate(update indexerbase.EntityUpdate) error { + ti, ok := i.tables[update.TableName] + if !ok { + return fmt.Errorf("table %s not found", update.TableName) + } + + err := indexerbase.ValidateKey(ti.table.KeyColumns, update.Key) + if err != nil { + fmt.Printf("error validating key: %s\n", err) + } + + if !update.Delete { + err = indexerbase.ValidateValue(ti.table.ValueColumns, update.Value) + if err != nil { + fmt.Printf("error validating value: %s\n", err) + } + } + + return ti.exec(i.ctx, i.conn, update) +} + +func (i *indexer) commit() error { + return nil +} From 6deca0110ec09f0a789666392ccee91b6b718d80 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 12 Jun 2024 15:36:00 -0400 Subject: [PATCH 06/79] WIP --- indexer/postgres/create_table.go | 110 ------------------------------- indexer/postgres/indexer.go | 102 ++++++++-------------------- indexer/postgres/meta_schema.sql | 5 ++ 3 files changed, 33 insertions(+), 184 deletions(-) delete mode 100644 indexer/postgres/create_table.go create mode 100644 indexer/postgres/meta_schema.sql diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go deleted file mode 100644 index 1cb15b9bf472..000000000000 --- a/indexer/postgres/create_table.go +++ /dev/null @@ -1,110 +0,0 @@ -package postgres - -import ( - "bytes" - "fmt" - "io" - "strings" - - indexerbase "cosmossdk.io/indexer/base" -) - -func (i *indexer) createTableStatement(tableSchema indexerbase.Table) (string, error) { - w := &bytes.Buffer{} - _, err := fmt.Fprintf(w, "CREATE TABLE IF NOT EXISTS %s (\n\t", tableSchema.Name) - if err != nil { - return "", err - } - - isSingleton := false - if len(tableSchema.KeyColumns) == 0 { - isSingleton = true - _, err = fmt.Fprintf(w, "_id INTEGER NOT NULL CHECK (_id = 1),\n\t") - } else { - for _, col := range tableSchema.KeyColumns { - err = i.createColumnDef(w, col) - if err != nil { - return "", err - } - } - } - - for _, col := range tableSchema.ValueColumns { - err = i.createColumnDef(w, col) - if err != nil { - return "", err - } - } - - var pKeys []string - if !isSingleton { - for _, col := range tableSchema.KeyColumns { - pKeys = append(pKeys, col.Name) - } - } else { - pKeys = []string{"_id"} - } - - _, err = fmt.Fprintf(w, "PRIMARY KEY (%s)\n", strings.Join(pKeys, ", ")) - if err != nil { - return "", err - } - - _, err = fmt.Fprintf(w, ");") - if err != nil { - return "", err - } - - return w.String(), nil -} - -func (i *indexer) createColumnDef(w io.Writer, col indexerbase.Column) error { - typeStr, err := i.colType(col) - if err != nil { - return err - } - - _, err = fmt.Fprintf(w, "%s %s,\n\t", col.Name, typeStr) - return err -} - -func (i *indexer) colType(col indexerbase.Column) (string, error) { - switch col.Type { - case indexerbase.StringKind: - return "TEXT", nil - case indexerbase.BoolKind: - return "BOOLEAN", nil - case indexerbase.BytesKind: - return "BYTEA", nil - case indexerbase.Int8Kind: - return "SMALLINT", nil - case indexerbase.Int16Kind: - return "SMALLINT", nil - case indexerbase.Int32Kind: - return "INTEGER", nil - case indexerbase.Int64Kind: - return "BIGINT", nil - case indexerbase.Uint8Kind: - return "SMALLINT", nil - case indexerbase.Uint16Kind: - return "INTEGER", nil - case indexerbase.Uint32Kind: - return "BIGINT", nil - case indexerbase.Uint64Kind: - return "NUMERIC", nil - case indexerbase.DecimalKind: - return "NUMERIC", nil - case indexerbase.Float32Kind: - return "REAL", nil - case indexerbase.Float64Kind: - return "DOUBLE PRECISION", nil - case indexerbase.EnumKind: - return "TEXT", fmt.Errorf("enums not supported yet") - case indexerbase.JSONKind: - return "JSONB", nil - case indexerbase.Bech32AddressKind: - return "TEXT", nil - default: - return "", fmt.Errorf("unsupported type %v", col.Type) - } -} diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index c8bd900d26a9..ce11b98275da 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -11,108 +11,62 @@ import ( ) type indexer struct { - ctx context.Context - conn *pgx.Conn - tables map[string]*tableInfo + ctx context.Context + conn *pgx.Conn } -type Options struct{} +type Options struct { + ConnectionURL string +} func NewIndexer(ctx context.Context, opts Options) (indexerbase.Listener, error) { - // get env var DATABASE_URL - dbUrl, ok := os.LookupEnv("DATABASE_URL") - if !ok { - panic("DATABASE_URL not set") + // get DATABASE_URL from environment + dbUrl := opts.ConnectionURL + if dbUrl == "" { + var ok bool + dbUrl, ok = os.LookupEnv("DATABASE_URL") + if !ok { + return indexerbase.Listener{}, fmt.Errorf("connection URL not set") + } } conn, err := pgx.Connect(ctx, dbUrl) if err != nil { - panic(err) + return indexerbase.Listener{}, err } i := &indexer{ - ctx: ctx, - conn: conn, - tables: map[string]*tableInfo{}, + ctx: ctx, + conn: conn, } + return i.logicalListener() } func (i *indexer) logicalListener() (indexerbase.Listener, error) { return indexerbase.Listener{ - StartBlock: i.startBlock, - Commit: i.commit, - //EnsureSetup: i.ensureSetup, - //OnEntityUpdate: i.onEntityUpdate, + Initialize: i.initialize, + InitializeModuleSchema: i.initModuleSchema, + StartBlock: i.startBlock, + Commit: i.commit, }, nil } -func (i *indexer) ensureSetup(data indexerbase.LogicalSetupData) error { - for _, table := range data.Schema.Tables { - createTable, err := i.createTableStatement(table) - if err != nil { - return err - } - fmt.Printf("%s\n", createTable) - _, err = i.conn.Exec(context.Background(), createTable) - if err != nil { - return err - } - - _, err = i.conn.Exec(context.Background(), fmt.Sprintf("GRANT SELECT ON %s TO public;", table.Name)) - if err != nil { - return err - } - - i.tables[table.Name] = &tableInfo{table: table} - } - return nil +func (i *indexer) initialize(indexerbase.InitializationData) (int64, error) { + // we don't care about persisting block data yet so just return 0 + return 0, nil } -type tableInfo struct { - table indexerbase.Table +func (i *indexer) initModuleSchema(moduleName string, schema indexerbase.ModuleSchema) (int64, error) { + //for _, _ := range schema.Tables { + //} + return -1, nil } func (i *indexer) startBlock(u uint64) error { return nil } -// func (i indexer) IndexBlockHeader(data *indexerbase.BlockHeaderData) error { -// //TODO implement me -// panic("implement me") -// } -// -// func (i indexer) IndexTx(data *indexerbase.TxData) error { -// //TODO implement me -// panic("implement me") -// } -// -// func (i indexer) IndexEvent(data *indexerbase.EventData) error { -// //TODO implement me -// panic("implement me") -// } - -func (i *indexer) onEntityUpdate(update indexerbase.EntityUpdate) error { - ti, ok := i.tables[update.TableName] - if !ok { - return fmt.Errorf("table %s not found", update.TableName) - } - - err := indexerbase.ValidateKey(ti.table.KeyColumns, update.Key) - if err != nil { - fmt.Printf("error validating key: %s\n", err) - } - - if !update.Delete { - err = indexerbase.ValidateValue(ti.table.ValueColumns, update.Value) - if err != nil { - fmt.Printf("error validating value: %s\n", err) - } - } - - return ti.exec(i.ctx, i.conn, update) -} - func (i *indexer) commit() error { return nil } diff --git a/indexer/postgres/meta_schema.sql b/indexer/postgres/meta_schema.sql new file mode 100644 index 000000000000..a582d21758dd --- /dev/null +++ b/indexer/postgres/meta_schema.sql @@ -0,0 +1,5 @@ +CREATE SCHEMA IF NOT EXISTS indexer; + +CREATE TABLE IF NOT EXISTS module_meta ( + module_name TEXT PRIMARY KEY, +); \ No newline at end of file From 4390b46d2b669eb487399090f3a3ce306b2c5348 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 11:09:49 -0400 Subject: [PATCH 07/79] WIP --- indexer/base/testing/testing.go | 190 ++++++++++++++++++++++++++++++-- 1 file changed, 178 insertions(+), 12 deletions(-) diff --git a/indexer/base/testing/testing.go b/indexer/base/testing/testing.go index f44a4cf0e620..cb4e4f2ac5e2 100644 --- a/indexer/base/testing/testing.go +++ b/indexer/base/testing/testing.go @@ -1,6 +1,13 @@ package indexertesting -import indexerbase "cosmossdk.io/indexer/base" +import ( + "encoding/json" + "fmt" + "strconv" + "time" + + indexerbase "cosmossdk.io/indexer/base" +) // ListenerTestFixture is a test fixture for testing listener implementations with a pre-defined data set // that attempts to cover all known types of tables and columns. The test data currently includes data for @@ -43,21 +50,180 @@ func (f *ListenerTestFixture) block3() error { var moduleSchemaA = indexerbase.ModuleSchema{ Tables: []indexerbase.Table{ { - "A1", - []indexerbase.Column{}, + "Singleton", []indexerbase.Column{}, + []indexerbase.Column{ + { + Name: "Value", + Kind: indexerbase.StringKind, + }, + }, false, }, - }, -} - -var moduleSchemaB = indexerbase.ModuleSchema{ - Tables: []indexerbase.Table{ { - "B1", - []indexerbase.Column{}, - []indexerbase.Column{}, - false, + Name: "Simple", + KeyColumns: []indexerbase.Column{ + { + Name: "Key", + Kind: indexerbase.StringKind, + }, + }, + ValueColumns: []indexerbase.Column{ + { + Name: "Value1", + Kind: indexerbase.Int32Kind, + }, + { + Name: "Value2", + Kind: indexerbase.BytesKind, + }, + }, + }, + { + Name: "Two Keys", + KeyColumns: []indexerbase.Column{ + { + Name: "Key1", + Kind: indexerbase.StringKind, + }, + { + Name: "Key2", + Kind: indexerbase.Int32Kind, + }, + }, + }, + { + Name: "Main Values", + }, + { + Name: "No Values", }, }, } + +var maxKind = indexerbase.JSONKind + +func mkTestModule() (indexerbase.ModuleSchema, func(seed int) []indexerbase.EntityUpdate) { + schema := indexerbase.ModuleSchema{} + for i := 1; i < int(maxKind); i++ { + schema.Tables = append(schema.Tables, mkTestTable(indexerbase.Kind(i))) + } + + return schema, func(seed int) []indexerbase.EntityUpdate { + panic("TODO") + } +} + +func mkTestTable(kind indexerbase.Kind) indexerbase.Table { + col := indexerbase.Column{ + Name: fmt.Sprintf("test_%s", kind), + Kind: kind, + } + + if kind == indexerbase.EnumKind { + col.EnumDefinition = testEnum + } + + if kind == indexerbase.Bech32AddressKind { + col.AddressPrefix = "cosmos" + } + + key1Col := col + key1Col.Name = "keyNotNull" + key2Col := col + key2Col.Name = "keyNullable" + key2Col.Nullable = true + val1Col := col + val1Col.Name = "valNotNull" + val2Col := col + val2Col.Name = "valNullable" + val2Col.Nullable = true + + return indexerbase.Table{ + Name: "KindTable", + KeyColumns: []indexerbase.Column{key1Col, key2Col}, + ValueColumns: []indexerbase.Column{val1Col, val2Col}, + } +} + +func mkTestUpdate(seed uint64, kind indexerbase.Kind) indexerbase.EntityUpdate { + update := indexerbase.EntityUpdate{} + + k1 := mkTestValue(seed, kind, false) + k2 := mkTestValue(seed+1, kind, true) + update.Key = []any{k1, k2} + + // delete 10% of the time + if seed%10 == 0 { + update.Delete = true + return update + } + + v1 := mkTestValue(seed+2, kind, false) + v2 := mkTestValue(seed+3, kind, true) + update.Value = []any{v1, v2} + + return update +} + +func mkTestValue(seed uint64, kind indexerbase.Kind, nullable bool) any { + // if it's nullable, return nil 10% of the time + if nullable && seed%10 == 1 { + return nil + } + + switch kind { + case indexerbase.StringKind: + // TODO fmt.Stringer + return "seed" + strconv.FormatUint(seed, 10) + case indexerbase.BytesKind: + return []byte("seed" + strconv.FormatUint(seed, 10)) + case indexerbase.Int8Kind: + return int8(seed) + case indexerbase.Int16Kind: + return int16(seed) + case indexerbase.Uint8Kind: + return uint8(seed) + case indexerbase.Uint16Kind: + return uint16(seed) + case indexerbase.Int32Kind: + return int32(seed) + case indexerbase.Uint32Kind: + return uint32(seed) + case indexerbase.Int64Kind: + return int64(seed) + case indexerbase.Uint64Kind: + return uint64(seed) + case indexerbase.IntegerKind: + // TODO fmt.Stringer, int64 + return fmt.Sprintf("%d", seed) + case indexerbase.DecimalKind: + // TODO fmt.Stringer + return fmt.Sprintf("%d.%d", seed, seed) + case indexerbase.BoolKind: + return seed%2 == 0 + case indexerbase.TimeKind: + return time.Unix(int64(seed), 0) + case indexerbase.DurationKind: + return time.Duration(seed) * time.Second + case indexerbase.Float32Kind: + return float32(seed) + case indexerbase.Float64Kind: + return float64(seed) + case indexerbase.Bech32AddressKind: + // TODO bytes + return "cosmos1address" + strconv.FormatUint(seed, 10) + case indexerbase.EnumKind: + return testEnum.Values[int(seed)%len(testEnum.Values)] + case indexerbase.JSONKind: + // TODO other types + return json.RawMessage(`{"seed": ` + strconv.FormatUint(seed, 10) + `}`) + default: + } + panic(fmt.Errorf("unexpected kind: %v", kind)) +} + +var testEnum = indexerbase.EnumDefinition{ + Name: "TestEnum", + Values: []string{"A", "B", "C"}, +} From 663ed17e3deca572e532e019b674ccae47657a82 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 11:26:30 -0400 Subject: [PATCH 08/79] rename column to field --- indexer/base/{column.go => field.go} | 78 +++++++++---------- .../base/{column_test.go => field_test.go} | 2 +- indexer/base/kind.go | 12 +-- indexer/base/table.go | 8 +- 4 files changed, 50 insertions(+), 50 deletions(-) rename indexer/base/{column.go => field.go} (53%) rename indexer/base/{column_test.go => field_test.go} (51%) diff --git a/indexer/base/column.go b/indexer/base/field.go similarity index 53% rename from indexer/base/column.go rename to indexer/base/field.go index 30b75d161c82..3dd3239c049c 100644 --- a/indexer/base/column.go +++ b/indexer/base/field.go @@ -2,18 +2,18 @@ package indexerbase import "fmt" -// Column represents a column in a table schema. -type Column struct { - // Name is the name of the column. +// Field represents a field in a table schema. +type Field struct { + // Name is the name of the field. Name string - // Kind is the basic type of the column. + // Kind is the basic type of the field. Kind Kind - // Nullable indicates whether null values are accepted for the column. + // Nullable indicates whether null values are accepted for the field. Nullable bool - // AddressPrefix is the address prefix of the column's kind, currently only used for Bech32AddressKind. + // AddressPrefix is the address prefix of the field's kind, currently only used for Bech32AddressKind. AddressPrefix string // EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind. @@ -29,32 +29,32 @@ type EnumDefinition struct { Values []string } -// Validate validates the column. -func (c Column) Validate() error { +// Validate validates the field. +func (c Field) Validate() error { // non-empty name if c.Name == "" { - return fmt.Errorf("column name cannot be empty") + return fmt.Errorf("field name cannot be empty") } // valid kind if err := c.Kind.Validate(); err != nil { - return fmt.Errorf("invalid column type for %q: %w", c.Name, err) + return fmt.Errorf("invalid field type for %q: %w", c.Name, err) } // address prefix only valid with Bech32AddressKind if c.Kind == Bech32AddressKind && c.AddressPrefix == "" { - return fmt.Errorf("missing address prefix for column %q", c.Name) + return fmt.Errorf("missing address prefix for field %q", c.Name) } else if c.Kind != Bech32AddressKind && c.AddressPrefix != "" { - return fmt.Errorf("address prefix is only valid for column %q with type Bech32AddressKind", c.Name) + return fmt.Errorf("address prefix is only valid for field %q with type Bech32AddressKind", c.Name) } // enum definition only valid with EnumKind if c.Kind == EnumKind { if err := c.EnumDefinition.Validate(); err != nil { - return fmt.Errorf("invalid enum definition for column %q: %w", c.Name, err) + return fmt.Errorf("invalid enum definition for field %q: %w", c.Name, err) } } else if c.Kind != EnumKind && c.EnumDefinition.Name != "" && c.EnumDefinition.Values != nil { - return fmt.Errorf("enum definition is only valid for column %q with type EnumKind", c.Name) + return fmt.Errorf("enum definition is only valid for field %q with type EnumKind", c.Name) } return nil @@ -81,63 +81,63 @@ func (e EnumDefinition) Validate() error { return nil } -// ValidateValue validates that the value conforms to the column's kind and nullability. +// ValidateValue validates that the value conforms to the field's kind and nullability. // It currently does not do any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind // values are valid for their respective types behind conforming to the correct go type. -func (c Column) ValidateValue(value any) error { +func (c Field) ValidateValue(value any) error { if value == nil { if !c.Nullable { - return fmt.Errorf("column %q cannot be null", c.Name) + return fmt.Errorf("field %q cannot be null", c.Name) } return nil } return c.Kind.ValidateValueType(value) } -// ValidateKey validates that the value conforms to the set of columns as a Key in an EntityUpdate. +// ValidateKey validates that the value conforms to the set of fields as a Key in an EntityUpdate. // See EntityUpdate.Key for documentation on the requirements of such values. -func ValidateKey(cols []Column, value any) error { - if len(cols) == 0 { +func ValidateKey(fields []Field, value any) error { + if len(fields) == 0 { return nil } - if len(cols) == 1 { - return cols[0].ValidateValue(value) + if len(fields) == 1 { + return fields[0].ValidateValue(value) } values, ok := value.([]any) if !ok { - return fmt.Errorf("expected slice of values for key columns, got %T", value) + return fmt.Errorf("expected slice of values for key fields, got %T", value) } - if len(cols) != len(values) { - return fmt.Errorf("expected %d key columns, got %d values", len(cols), len(value.([]any))) + if len(fields) != len(values) { + return fmt.Errorf("expected %d key fields, got %d values", len(fields), len(value.([]any))) } - for i, col := range cols { - if err := col.ValidateValue(values[i]); err != nil { - return fmt.Errorf("invalid value for key column %q: %w", col.Name, err) + for i, field := range fields { + if err := field.ValidateValue(values[i]); err != nil { + return fmt.Errorf("invalid value for key field %q: %w", field.Name, err) } } return nil } -// ValidateValue validates that the value conforms to the set of columns as a Value in an EntityUpdate. +// ValidateValue validates that the value conforms to the set of fields as a Value in an EntityUpdate. // See EntityUpdate.Value for documentation on the requirements of such values. -func ValidateValue(cols []Column, value any) error { +func ValidateValue(fields []Field, value any) error { valueUpdates, ok := value.(ValueUpdates) if ok { - colMap := map[string]Column{} - for _, col := range cols { - colMap[col.Name] = col + fieldMap := map[string]Field{} + for _, field := range fields { + fieldMap[field.Name] = field } var errs []error - valueUpdates.Iterate(func(colName string, value any) bool { - col, ok := colMap[colName] + valueUpdates.Iterate(func(fieldName string, value any) bool { + field, ok := fieldMap[fieldName] if !ok { - errs = append(errs, fmt.Errorf("unknown column %q in value updates", colName)) + errs = append(errs, fmt.Errorf("unknown field %q in value updates", fieldName)) } - if err := col.ValidateValue(value); err != nil { - errs = append(errs, fmt.Errorf("invalid value for column %q: %w", colName, err)) + if err := field.ValidateValue(value); err != nil { + errs = append(errs, fmt.Errorf("invalid value for field %q: %w", fieldName, err)) } return true }) @@ -146,6 +146,6 @@ func ValidateValue(cols []Column, value any) error { } return nil } else { - return ValidateKey(cols, value) + return ValidateKey(fields, value) } } diff --git a/indexer/base/column_test.go b/indexer/base/field_test.go similarity index 51% rename from indexer/base/column_test.go rename to indexer/base/field_test.go index b646247b058c..09f4488bef38 100644 --- a/indexer/base/column_test.go +++ b/indexer/base/field_test.go @@ -2,6 +2,6 @@ package indexerbase import "testing" -func TestColumnValidate(t *testing.T) { +func TestField_Validate(t *testing.T) { } diff --git a/indexer/base/kind.go b/indexer/base/kind.go index 0c86ab90f4c1..bb888e3d8a1f 100644 --- a/indexer/base/kind.go +++ b/indexer/base/kind.go @@ -6,7 +6,7 @@ import ( "time" ) -// Kind represents the basic type of a column in the table schema. +// Kind represents the basic type of a field in the table schema. // Each kind defines the types of go values which should be accepted // by listeners and generated by decoders when providing entity updates. type Kind int @@ -73,12 +73,12 @@ const ( Float64Kind // Bech32AddressKind is a bech32 address type and values of this type must be of the go type string or []byte - // or a type which implements fmt.Stringer. Columns of this type are expected to set the AddressPrefix field - // in the column definition to the bech32 address prefix. + // or a type which implements fmt.Stringer. Fields of this type are expected to set the AddressPrefix field + // in the field definition to the bech32 address prefix. Bech32AddressKind // EnumKind is an enum type and values of this type must be of the go type string or implement fmt.Stringer. - // Columns of this type are expected to set the EnumDefinition field in the column definition to the enum + // Fields of this type are expected to set the EnumDefinition field in the field definition to the enum // definition. EnumKind @@ -99,7 +99,7 @@ func (t Kind) Validate() error { } // ValidateValueType returns an error if the value does not the type go type specified by the kind. -// Some columns may accept nil values, however, this method does not have any notion of +// Some fields may accept nil values, however, this method does not have any notion of // nullability. It only checks that the value is of the correct type. // It also doesn't perform any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind // values are valid for their respective types. @@ -268,7 +268,7 @@ func (t Kind) String() string { // represented as strings. Generally all values which do not have a more specific type will // return JSONKind because the framework cannot decide at this point whether the value // can or cannot be marshaled to JSON. This method should generally only be used as a fallback -// when the kind of a column is not specified more specifically. +// when the kind of a field is not specified more specifically. func KindForGoValue(value any) Kind { switch value.(type) { case string, fmt.Stringer: diff --git a/indexer/base/table.go b/indexer/base/table.go index 2d076f6d3eb8..350d961c2411 100644 --- a/indexer/base/table.go +++ b/indexer/base/table.go @@ -5,11 +5,11 @@ type Table struct { // Name is the name of the table. Name string - // KeyColumns is a list of columns that make up the primary key of the table. - KeyColumns []Column + // KeyFields is a list of fields that make up the primary key of the table. + KeyFields []Field - // ValueColumns is a list of columns that are not part of the primary key of the table. - ValueColumns []Column + // ValueFields is a list of fields that are not part of the primary key of the table. + ValueFields []Field // RetainDeletions is a flag that indicates whether the indexer should retain // deleted rows in the database and flag them as deleted rather than actually From 43113570d1b5da6b11088830c2447c08b271f99c Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 11:39:06 -0400 Subject: [PATCH 09/79] delete code, simplify --- indexer/base/field.go | 123 ---------------- indexer/base/field_test.go | 7 - indexer/base/kind.go | 227 ----------------------------- indexer/base/kind_test.go | 290 ------------------------------------- indexer/base/listener.go | 18 +-- 5 files changed, 5 insertions(+), 660 deletions(-) delete mode 100644 indexer/base/field_test.go delete mode 100644 indexer/base/kind_test.go diff --git a/indexer/base/field.go b/indexer/base/field.go index 3dd3239c049c..93755f47e6c4 100644 --- a/indexer/base/field.go +++ b/indexer/base/field.go @@ -1,7 +1,5 @@ package indexerbase -import "fmt" - // Field represents a field in a table schema. type Field struct { // Name is the name of the field. @@ -28,124 +26,3 @@ type EnumDefinition struct { // Values is a list of distinct values that are part of the enum type. Values []string } - -// Validate validates the field. -func (c Field) Validate() error { - // non-empty name - if c.Name == "" { - return fmt.Errorf("field name cannot be empty") - } - - // valid kind - if err := c.Kind.Validate(); err != nil { - return fmt.Errorf("invalid field type for %q: %w", c.Name, err) - } - - // address prefix only valid with Bech32AddressKind - if c.Kind == Bech32AddressKind && c.AddressPrefix == "" { - return fmt.Errorf("missing address prefix for field %q", c.Name) - } else if c.Kind != Bech32AddressKind && c.AddressPrefix != "" { - return fmt.Errorf("address prefix is only valid for field %q with type Bech32AddressKind", c.Name) - } - - // enum definition only valid with EnumKind - if c.Kind == EnumKind { - if err := c.EnumDefinition.Validate(); err != nil { - return fmt.Errorf("invalid enum definition for field %q: %w", c.Name, err) - } - } else if c.Kind != EnumKind && c.EnumDefinition.Name != "" && c.EnumDefinition.Values != nil { - return fmt.Errorf("enum definition is only valid for field %q with type EnumKind", c.Name) - } - - return nil -} - -// Validate validates the enum definition. -func (e EnumDefinition) Validate() error { - if e.Name == "" { - return fmt.Errorf("enum definition name cannot be empty") - } - if len(e.Values) == 0 { - return fmt.Errorf("enum definition values cannot be empty") - } - seen := make(map[string]bool, len(e.Values)) - for i, v := range e.Values { - if v == "" { - return fmt.Errorf("enum definition value at index %d cannot be empty for enum %s", i, e.Name) - } - if seen[v] { - return fmt.Errorf("duplicate enum definition value %q for enum %s", v, e.Name) - } - seen[v] = true - } - return nil -} - -// ValidateValue validates that the value conforms to the field's kind and nullability. -// It currently does not do any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind -// values are valid for their respective types behind conforming to the correct go type. -func (c Field) ValidateValue(value any) error { - if value == nil { - if !c.Nullable { - return fmt.Errorf("field %q cannot be null", c.Name) - } - return nil - } - return c.Kind.ValidateValueType(value) -} - -// ValidateKey validates that the value conforms to the set of fields as a Key in an EntityUpdate. -// See EntityUpdate.Key for documentation on the requirements of such values. -func ValidateKey(fields []Field, value any) error { - if len(fields) == 0 { - return nil - } - - if len(fields) == 1 { - return fields[0].ValidateValue(value) - } - - values, ok := value.([]any) - if !ok { - return fmt.Errorf("expected slice of values for key fields, got %T", value) - } - - if len(fields) != len(values) { - return fmt.Errorf("expected %d key fields, got %d values", len(fields), len(value.([]any))) - } - for i, field := range fields { - if err := field.ValidateValue(values[i]); err != nil { - return fmt.Errorf("invalid value for key field %q: %w", field.Name, err) - } - } - return nil -} - -// ValidateValue validates that the value conforms to the set of fields as a Value in an EntityUpdate. -// See EntityUpdate.Value for documentation on the requirements of such values. -func ValidateValue(fields []Field, value any) error { - valueUpdates, ok := value.(ValueUpdates) - if ok { - fieldMap := map[string]Field{} - for _, field := range fields { - fieldMap[field.Name] = field - } - var errs []error - valueUpdates.Iterate(func(fieldName string, value any) bool { - field, ok := fieldMap[fieldName] - if !ok { - errs = append(errs, fmt.Errorf("unknown field %q in value updates", fieldName)) - } - if err := field.ValidateValue(value); err != nil { - errs = append(errs, fmt.Errorf("invalid value for field %q: %w", fieldName, err)) - } - return true - }) - if len(errs) > 0 { - return fmt.Errorf("validation errors: %v", errs) - } - return nil - } else { - return ValidateKey(fields, value) - } -} diff --git a/indexer/base/field_test.go b/indexer/base/field_test.go deleted file mode 100644 index 09f4488bef38..000000000000 --- a/indexer/base/field_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package indexerbase - -import "testing" - -func TestField_Validate(t *testing.T) { - -} diff --git a/indexer/base/kind.go b/indexer/base/kind.go index bb888e3d8a1f..ba4046ba330b 100644 --- a/indexer/base/kind.go +++ b/indexer/base/kind.go @@ -1,11 +1,5 @@ package indexerbase -import ( - "encoding/json" - "fmt" - "time" -) - // Kind represents the basic type of a field in the table schema. // Each kind defines the types of go values which should be accepted // by listeners and generated by decoders when providing entity updates. @@ -86,224 +80,3 @@ const ( // or any type that can be marshaled to JSON using json.Marshal. JSONKind ) - -// Validate returns an error if the kind is invalid. -func (t Kind) Validate() error { - if t <= InvalidKind { - return fmt.Errorf("unknown type: %d", t) - } - if t > JSONKind { - return fmt.Errorf("invalid type: %d", t) - } - return nil -} - -// ValidateValueType returns an error if the value does not the type go type specified by the kind. -// Some fields may accept nil values, however, this method does not have any notion of -// nullability. It only checks that the value is of the correct type. -// It also doesn't perform any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind -// values are valid for their respective types. -func (t Kind) ValidateValueType(value any) error { - switch t { - case StringKind: - _, ok := value.(string) - _, ok2 := value.(fmt.Stringer) - if !ok && !ok2 { - return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) - } - case BytesKind: - _, ok := value.([]byte) - if !ok { - return fmt.Errorf("expected []byte, got %T", value) - } - case Int8Kind: - _, ok := value.(int8) - if !ok { - return fmt.Errorf("expected int8, got %T", value) - } - case Uint8Kind: - _, ok := value.(uint8) - if !ok { - return fmt.Errorf("expected uint8, got %T", value) - } - case Int16Kind: - _, ok := value.(int16) - if !ok { - return fmt.Errorf("expected int16, got %T", value) - } - case Uint16Kind: - _, ok := value.(uint16) - if !ok { - return fmt.Errorf("expected uint16, got %T", value) - } - case Int32Kind: - _, ok := value.(int32) - if !ok { - return fmt.Errorf("expected int32, got %T", value) - } - case Uint32Kind: - _, ok := value.(uint32) - if !ok { - return fmt.Errorf("expected uint32, got %T", value) - } - case Int64Kind: - _, ok := value.(int64) - if !ok { - return fmt.Errorf("expected int64, got %T", value) - } - case Uint64Kind: - _, ok := value.(uint64) - if !ok { - return fmt.Errorf("expected uint64, got %T", value) - } - case IntegerKind: - _, ok := value.(string) - _, ok2 := value.(fmt.Stringer) - _, ok3 := value.(int64) - if !ok && !ok2 && !ok3 { - return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) - } - case DecimalKind: - _, ok := value.(string) - _, ok2 := value.(fmt.Stringer) - if !ok && !ok2 { - return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) - } - case BoolKind: - _, ok := value.(bool) - if !ok { - return fmt.Errorf("expected bool, got %T", value) - } - case TimeKind: - _, ok := value.(time.Time) - if !ok { - return fmt.Errorf("expected time.Time, got %T", value) - } - case DurationKind: - _, ok := value.(time.Duration) - if !ok { - return fmt.Errorf("expected time.Duration, got %T", value) - } - case Float32Kind: - _, ok := value.(float32) - if !ok { - return fmt.Errorf("expected float32, got %T", value) - } - case Float64Kind: - _, ok := value.(float64) - if !ok { - return fmt.Errorf("expected float64, got %T", value) - } - case Bech32AddressKind: - _, ok := value.(string) - _, ok2 := value.([]byte) - _, ok3 := value.(fmt.Stringer) - if !ok && !ok2 && !ok3 { - return fmt.Errorf("expected string or []byte, got %T", value) - } - case EnumKind: - _, ok := value.(string) - _, ok2 := value.(fmt.Stringer) - if !ok && !ok2 { - return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) - } - case JSONKind: - return nil - default: - return fmt.Errorf("invalid type: %d", t) - } - return nil -} - -// String returns a string representation of the kind. -func (t Kind) String() string { - switch t { - case StringKind: - return "string" - case BytesKind: - return "bytes" - case Int8Kind: - return "int8" - case Uint8Kind: - return "uint8" - case Int16Kind: - return "int16" - case Uint16Kind: - return "uint16" - case Int32Kind: - return "int32" - case Uint32Kind: - return "uint32" - case Int64Kind: - return "int64" - case Uint64Kind: - return "uint64" - case DecimalKind: - return "decimal" - case IntegerKind: - return "integer" - case BoolKind: - return "bool" - case TimeKind: - return "time" - case DurationKind: - return "duration" - case Float32Kind: - return "float32" - case Float64Kind: - return "float64" - case Bech32AddressKind: - return "bech32address" - case EnumKind: - return "enum" - case JSONKind: - return "json" - default: - return "" - } -} - -// KindForGoValue finds the simplest kind that can represent the given go value. It will not, however, -// return kinds such as IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind which all can be -// represented as strings. Generally all values which do not have a more specific type will -// return JSONKind because the framework cannot decide at this point whether the value -// can or cannot be marshaled to JSON. This method should generally only be used as a fallback -// when the kind of a field is not specified more specifically. -func KindForGoValue(value any) Kind { - switch value.(type) { - case string, fmt.Stringer: - return StringKind - case []byte: - return BytesKind - case int8: - return Int8Kind - case uint8: - return Uint8Kind - case int16: - return Int16Kind - case uint16: - return Uint16Kind - case int32: - return Int32Kind - case uint32: - return Uint32Kind - case int64: - return Int64Kind - case uint64: - return Uint64Kind - case float32: - return Float32Kind - case float64: - return Float64Kind - case bool: - return BoolKind - case time.Time: - return TimeKind - case time.Duration: - return DurationKind - case json.RawMessage: - return JSONKind - default: - return JSONKind - } -} diff --git a/indexer/base/kind_test.go b/indexer/base/kind_test.go deleted file mode 100644 index 18075535b067..000000000000 --- a/indexer/base/kind_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package indexerbase - -import ( - "strings" - "testing" - "time" -) - -func TestKind_Validate(t *testing.T) { - validKinds := []Kind{ - StringKind, - BytesKind, - Int8Kind, - Uint8Kind, - Int16Kind, - Uint16Kind, - Int32Kind, - Uint32Kind, - Int64Kind, - Uint64Kind, - IntegerKind, - DecimalKind, - BoolKind, - EnumKind, - Bech32AddressKind, - } - - for _, kind := range validKinds { - if err := kind.Validate(); err != nil { - t.Errorf("expected valid kind %s to pass validation, got: %v", kind, err) - } - } - - invalidKinds := []Kind{ - Kind(-1), - InvalidKind, - Kind(100), - } - - for _, kind := range invalidKinds { - if err := kind.Validate(); err == nil { - t.Errorf("expected invalid kind %s to fail validation, got: %v", kind, err) - } - } -} - -func TestKind_ValidateValue(t *testing.T) { - tests := []struct { - kind Kind - value any - valid bool - }{ - { - kind: StringKind, - value: "hello", - valid: true, - }, - { - kind: StringKind, - value: &strings.Builder{}, - valid: true, - }, - { - kind: StringKind, - value: []byte("hello"), - valid: false, - }, - { - kind: BytesKind, - value: []byte("hello"), - valid: true, - }, - { - kind: BytesKind, - value: "hello", - valid: false, - }, - { - kind: Int8Kind, - value: int8(1), - valid: true, - }, - { - kind: Int8Kind, - value: int16(1), - valid: false, - }, - { - kind: Uint8Kind, - value: uint8(1), - valid: true, - }, - { - kind: Uint8Kind, - value: uint16(1), - valid: false, - }, - { - kind: Int16Kind, - value: int16(1), - valid: true, - }, - { - kind: Int16Kind, - value: int32(1), - valid: false, - }, - { - kind: Uint16Kind, - value: uint16(1), - valid: true, - }, - { - kind: Uint16Kind, - value: uint32(1), - valid: false, - }, - { - kind: Int32Kind, - value: int32(1), - valid: true, - }, - { - kind: Int32Kind, - value: int64(1), - valid: false, - }, - { - kind: Uint32Kind, - value: uint32(1), - valid: true, - }, - { - kind: Uint32Kind, - value: uint64(1), - valid: false, - }, - { - kind: Int64Kind, - value: int64(1), - valid: true, - }, - { - kind: Int64Kind, - value: int32(1), - valid: false, - }, - { - kind: Uint64Kind, - value: uint64(1), - valid: true, - }, - { - kind: Uint64Kind, - value: uint32(1), - valid: false, - }, - { - kind: IntegerKind, - value: "1", - valid: true, - }, - //{ - // kind: IntegerKind, - // value: (&strings.Builder{}).WriteString("1"), - // valid: true, - //}, - { - kind: IntegerKind, - value: int32(1), - valid: false, - }, - { - kind: IntegerKind, - value: int64(1), - valid: true, - }, - { - kind: DecimalKind, - value: "1.0", - valid: true, - }, - { - kind: DecimalKind, - value: "1", - valid: true, - }, - { - kind: DecimalKind, - value: "1.1e4", - valid: true, - }, - //{ - // kind: DecimalKind, - // value: (&strings.Builder{}).WriteString("1.0"), - // valid: true, - //}, - { - kind: DecimalKind, - value: int32(1), - valid: false, - }, - { - kind: Bech32AddressKind, - value: "cosmos1hsk6jryyqjfhp5g7c0nh4n6dd45ygctnxglp5h", - valid: true, - }, - //{ - // kind: Bech32AddressKind, - // value: (&strings.Builder{}).WriteString("cosmos1hsk6jryyqjfhp5g7c0nh4n6dd45ygctnxglp5h"), - // valid: true, - //}, - { - kind: Bech32AddressKind, - value: 1, - valid: false, - }, - { - kind: BoolKind, - value: true, - valid: true, - }, - { - kind: BoolKind, - value: false, - valid: true, - }, - { - kind: BoolKind, - value: 1, - valid: false, - }, - { - kind: EnumKind, - value: "hello", - valid: true, - }, - //{ - // kind: EnumKind, - // value: (&strings.Builder{}).WriteString("hello"), - // valid: true, - //}, - { - kind: EnumKind, - value: 1, - valid: false, - }, - { - kind: TimeKind, - value: time.Now(), - valid: true, - }, - { - kind: TimeKind, - value: "hello", - valid: false, - }, - { - kind: DurationKind, - value: time.Second, - valid: true, - }, - { - kind: DurationKind, - value: "hello", - valid: false, - }, - { - kind: Float32Kind, - value: float32(1.0), - valid: true, - }, - { - kind: Float32Kind, - value: float64(1.0), - valid: false, - }, - // TODO float64, json - } - - for i, tt := range tests { - err := tt.kind.ValidateValueType(tt.value) - if tt.valid && err != nil { - t.Errorf("test %d: expected valid value %v for kind %s to pass validation, got: %v", i, tt.value, tt.kind, err) - } - if !tt.valid && err == nil { - t.Errorf("test %d: expected invalid value %v for kind %s to fail validation, got: %v", i, tt.value, tt.kind, err) - } - } -} diff --git a/indexer/base/listener.go b/indexer/base/listener.go index 05b8de2a5013..493f1269096f 100644 --- a/indexer/base/listener.go +++ b/indexer/base/listener.go @@ -12,10 +12,11 @@ import ( // These methods will only be called when listening logical decoding is setup. type Listener struct { // Initialize is called when the listener is initialized before any other methods are called. - // The lastBlock return value should be the last block height the listener persisted if it is + // The lastBlockPersisted return value should be the last block height the listener persisted if it is // persisting block data, 0 if it is not interested in persisting block data, or -1 if it is - // persisting block data but has not persisted any data yet. - Initialize func(InitializationData) (lastBlock int64, err error) + // persisting block data but has not persisted any data yet. This check allows the indexer + // framework to ensure that the listener has not missed blocks. + Initialize func(InitializationData) (lastBlockPersisted int64, err error) // StartBlock is called at the beginning of processing a block. StartBlock func(uint64) error @@ -43,16 +44,7 @@ type Listener struct { // migrations) required to receive OnEntityUpdate events for the given module. If the // indexer's schema is incompatible with the module's on-chain schema, the listener should return // an error. - // If the listener is persisting state for the module, it should return the last block - // that was saved for the module so that the framework can determine whether it is safe - // to resume indexing from the current height or whether there is a gap (usually an error). - // If the listener does not persist any state for the module, it should return 0 for lastBlock - // and nil for error. - // If the listener has initialized properly and would like to persist state for the module, - // but does not have any persisted state yet, it should return -1 for lastBlock and nil for error. - // In this case, the framework will perform a "catch-up sync" calling OnEntityUpdate for every - // entity already in the module followed by CommitCatchupSync before processing new block data. - InitializeModuleSchema func(module string, schema ModuleSchema) (lastBlock int64, err error) + InitializeModuleSchema func(module string, schema ModuleSchema) error // OnEntityUpdate is called whenever an entity is updated in the module. This is only called // when logical data is available. It should be assumed that the same data in raw form From c52655a79c62f4b9bf4344b045e504f6e467c654 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 11:42:26 -0400 Subject: [PATCH 10/79] add error return --- indexer/base/entity.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/indexer/base/entity.go b/indexer/base/entity.go index 95f016037fd8..df2ee2d3dd2f 100644 --- a/indexer/base/entity.go +++ b/indexer/base/entity.go @@ -35,6 +35,7 @@ type ValueUpdates interface { // Iterate iterates over the columns and values in the entity update. The function should return // true to continue iteration or false to stop iteration. Each column value should conform - // to the requirements of that column's type in the schema. - Iterate(func(col string, value any) bool) + // to the requirements of that column's type in the schema. Iterate returns an error if + // it was unable to decode the values properly (which could be the case in lazy evaluation). + Iterate(func(col string, value any) bool) error } From 46669d35742ed17c39383d9fe92490faeb6f17bd Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 11:53:07 -0400 Subject: [PATCH 11/79] remove ability to filter subscribed modules - this is a bit dangerous --- indexer/base/listener.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/indexer/base/listener.go b/indexer/base/listener.go index 493f1269096f..43a66f450e76 100644 --- a/indexer/base/listener.go +++ b/indexer/base/listener.go @@ -57,11 +57,6 @@ type Listener struct { // this point and also save the block number as the last block that has been processed so // that processing of regular block data can resume from this point in the future. CommitCatchupSync func(module string, block uint64) error - - // SubscribedModules is a map of modules that the listener is interested in receiving events for in OnKVPair and - // logical decoding listeners (if these are registered). If this is left nil but listeners are registered, - // it is assumed that the listener is interested in all modules. - SubscribedModules map[string]bool } // InitializationData represents initialization data that is passed to a listener. From b7b6914dfe13054f63113b9cfd2dfe2ae2b53db9 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 11:55:53 -0400 Subject: [PATCH 12/79] fixes --- indexer/base/table.go | 8 ++---- indexer/base/testing/testing.go | 44 ++++++++++++++++----------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/indexer/base/table.go b/indexer/base/table.go index c9d21a4e6491..f2061005cd9a 100644 --- a/indexer/base/table.go +++ b/indexer/base/table.go @@ -6,17 +6,13 @@ type Table struct { Name string // KeyFields is a list of fields that make up the primary key of the table. - KeyFields []Field - // KeyColumns is a list of columns that make up the primary key of the table. // It can be empty in which case indexers should assume that this table is // a singleton and ony has one value. - KeyColumns []Column + KeyFields []Field // ValueFields is a list of fields that are not part of the primary key of the table. + // It can be empty in the case where all fields are part of the primary key. ValueFields []Field - // ValueColumns is a list of columns that are not part of the primary key of the table. - // It can be empty in the case where all columns are part of the primary key. - ValueColumns []Column // RetainDeletions is a flag that indicates whether the indexer should retain // deleted rows in the database and flag them as deleted rather than actually diff --git a/indexer/base/testing/testing.go b/indexer/base/testing/testing.go index cb4e4f2ac5e2..db3b14fd81e3 100644 --- a/indexer/base/testing/testing.go +++ b/indexer/base/testing/testing.go @@ -10,7 +10,7 @@ import ( ) // ListenerTestFixture is a test fixture for testing listener implementations with a pre-defined data set -// that attempts to cover all known types of tables and columns. The test data currently includes data for +// that attempts to cover all known types of tables and fields. The test data currently includes data for // two fake modules over three blocks of data. The data set should remain relatively stable between releases // and generally only be changed when new features are added, so it should be suitable for regression or golden tests. type ListenerTestFixture struct { @@ -51,8 +51,8 @@ var moduleSchemaA = indexerbase.ModuleSchema{ Tables: []indexerbase.Table{ { "Singleton", - []indexerbase.Column{}, - []indexerbase.Column{ + []indexerbase.Field{}, + []indexerbase.Field{ { Name: "Value", Kind: indexerbase.StringKind, @@ -62,13 +62,13 @@ var moduleSchemaA = indexerbase.ModuleSchema{ }, { Name: "Simple", - KeyColumns: []indexerbase.Column{ + KeyFields: []indexerbase.Field{ { Name: "Key", Kind: indexerbase.StringKind, }, }, - ValueColumns: []indexerbase.Column{ + ValueFields: []indexerbase.Field{ { Name: "Value1", Kind: indexerbase.Int32Kind, @@ -81,7 +81,7 @@ var moduleSchemaA = indexerbase.ModuleSchema{ }, { Name: "Two Keys", - KeyColumns: []indexerbase.Column{ + KeyFields: []indexerbase.Field{ { Name: "Key1", Kind: indexerbase.StringKind, @@ -115,34 +115,34 @@ func mkTestModule() (indexerbase.ModuleSchema, func(seed int) []indexerbase.Enti } func mkTestTable(kind indexerbase.Kind) indexerbase.Table { - col := indexerbase.Column{ + field := indexerbase.Field{ Name: fmt.Sprintf("test_%s", kind), Kind: kind, } if kind == indexerbase.EnumKind { - col.EnumDefinition = testEnum + field.EnumDefinition = testEnum } if kind == indexerbase.Bech32AddressKind { - col.AddressPrefix = "cosmos" + field.AddressPrefix = "cosmos" } - key1Col := col - key1Col.Name = "keyNotNull" - key2Col := col - key2Col.Name = "keyNullable" - key2Col.Nullable = true - val1Col := col - val1Col.Name = "valNotNull" - val2Col := col - val2Col.Name = "valNullable" - val2Col.Nullable = true + key1Field := field + key1Field.Name = "keyNotNull" + key2Field := field + key2Field.Name = "keyNullable" + key2Field.Nullable = true + val1Field := field + val1Field.Name = "valNotNull" + val2Field := field + val2Field.Name = "valNullable" + val2Field.Nullable = true return indexerbase.Table{ - Name: "KindTable", - KeyColumns: []indexerbase.Column{key1Col, key2Col}, - ValueColumns: []indexerbase.Column{val1Col, val2Col}, + Name: "KindTable", + KeyFields: []indexerbase.Field{key1Field, key2Field}, + ValueFields: []indexerbase.Field{val1Field, val2Field}, } } From 0a47c393ac374be23d13deae32b8e2024de2361d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 11:56:17 -0400 Subject: [PATCH 13/79] add docs about fields --- indexer/base/table.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/indexer/base/table.go b/indexer/base/table.go index 350d961c2411..f2061005cd9a 100644 --- a/indexer/base/table.go +++ b/indexer/base/table.go @@ -6,9 +6,12 @@ type Table struct { Name string // KeyFields is a list of fields that make up the primary key of the table. + // It can be empty in which case indexers should assume that this table is + // a singleton and ony has one value. KeyFields []Field // ValueFields is a list of fields that are not part of the primary key of the table. + // It can be empty in the case where all fields are part of the primary key. ValueFields []Field // RetainDeletions is a flag that indicates whether the indexer should retain From 0f07bafef96857405b3595379759e4fbe8849cb4 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 12:14:18 -0400 Subject: [PATCH 14/79] updates --- indexer/base/testing/testing.go | 92 ++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/indexer/base/testing/testing.go b/indexer/base/testing/testing.go index db3b14fd81e3..4c0812bf7aa6 100644 --- a/indexer/base/testing/testing.go +++ b/indexer/base/testing/testing.go @@ -1,9 +1,8 @@ package indexertesting import ( - "encoding/json" "fmt" - "strconv" + "math/rand" "time" indexerbase "cosmossdk.io/indexer/base" @@ -103,14 +102,22 @@ var moduleSchemaA = indexerbase.ModuleSchema{ var maxKind = indexerbase.JSONKind -func mkTestModule() (indexerbase.ModuleSchema, func(seed int) []indexerbase.EntityUpdate) { +func mkTestModule() (indexerbase.ModuleSchema, func(*rand.Rand) []indexerbase.EntityUpdate) { schema := indexerbase.ModuleSchema{} for i := 1; i < int(maxKind); i++ { schema.Tables = append(schema.Tables, mkTestTable(indexerbase.Kind(i))) } - return schema, func(seed int) []indexerbase.EntityUpdate { - panic("TODO") + return schema, func(rnd *rand.Rand) []indexerbase.EntityUpdate { + var updates []indexerbase.EntityUpdate + for i := 1; i < int(maxKind); i++ { + // 0-10 updates per kind + n := int(rnd.Int31n(11)) + for j := 0; j < n; j++ { + updates = append(updates, mkTestUpdate(rnd, indexerbase.Kind(i))) + } + } + return updates } } @@ -146,83 +153,94 @@ func mkTestTable(kind indexerbase.Kind) indexerbase.Table { } } -func mkTestUpdate(seed uint64, kind indexerbase.Kind) indexerbase.EntityUpdate { +func mkTestUpdate(rnd *rand.Rand, kind indexerbase.Kind) indexerbase.EntityUpdate { update := indexerbase.EntityUpdate{} - k1 := mkTestValue(seed, kind, false) - k2 := mkTestValue(seed+1, kind, true) + k1 := mkTestValue(rnd, kind, false) + k2 := mkTestValue(rnd, kind, true) update.Key = []any{k1, k2} // delete 10% of the time - if seed%10 == 0 { + if rnd.Int31n(10) == 1 { update.Delete = true return update } - v1 := mkTestValue(seed+2, kind, false) - v2 := mkTestValue(seed+3, kind, true) + v1 := mkTestValue(rnd, kind, false) + v2 := mkTestValue(rnd, kind, true) update.Value = []any{v1, v2} return update } -func mkTestValue(seed uint64, kind indexerbase.Kind, nullable bool) any { +func mkTestValue(rnd *rand.Rand, kind indexerbase.Kind, nullable bool) any { // if it's nullable, return nil 10% of the time - if nullable && seed%10 == 1 { + if nullable && rnd.Int31n(10) == 1 { return nil } switch kind { case indexerbase.StringKind: // TODO fmt.Stringer - return "seed" + strconv.FormatUint(seed, 10) + return string(randBz(rnd)) case indexerbase.BytesKind: - return []byte("seed" + strconv.FormatUint(seed, 10)) + return randBz(rnd) case indexerbase.Int8Kind: - return int8(seed) + return int8(rnd.Int31n(256) - 128) case indexerbase.Int16Kind: - return int16(seed) + return int16(rnd.Int31n(65536) - 32768) case indexerbase.Uint8Kind: - return uint8(seed) + return uint8(rnd.Int31n(256)) case indexerbase.Uint16Kind: - return uint16(seed) + return uint16(rnd.Int31n(65536)) case indexerbase.Int32Kind: - return int32(seed) + return int32(rnd.Int63n(4294967296) - 2147483648) case indexerbase.Uint32Kind: - return uint32(seed) + return uint32(rnd.Int63n(4294967296)) case indexerbase.Int64Kind: - return int64(seed) + return rnd.Int63() case indexerbase.Uint64Kind: - return uint64(seed) + return rnd.Uint64() case indexerbase.IntegerKind: - // TODO fmt.Stringer, int64 - return fmt.Sprintf("%d", seed) + x := rnd.Int63() + return fmt.Sprintf("%d", x) case indexerbase.DecimalKind: - // TODO fmt.Stringer - return fmt.Sprintf("%d.%d", seed, seed) + x := rnd.Int63() + y := rnd.Int63n(1000000000) + return fmt.Sprintf("%d.%d", x, y) case indexerbase.BoolKind: - return seed%2 == 0 + return rnd.Int31n(2) == 1 case indexerbase.TimeKind: - return time.Unix(int64(seed), 0) + return time.Unix(rnd.Int63(), rnd.Int63n(1000000000)) case indexerbase.DurationKind: - return time.Duration(seed) * time.Second + return time.Duration(rnd.Int63()) case indexerbase.Float32Kind: - return float32(seed) + return float32(rnd.Float64()) case indexerbase.Float64Kind: - return float64(seed) + return rnd.Float64() case indexerbase.Bech32AddressKind: - // TODO bytes - return "cosmos1address" + strconv.FormatUint(seed, 10) + panic("TODO: select from some actually valid known bech32 address strings and bytes") case indexerbase.EnumKind: - return testEnum.Values[int(seed)%len(testEnum.Values)] + return testEnum.Values[rnd.Int31n(int32(len(testEnum.Values)))] case indexerbase.JSONKind: - // TODO other types - return json.RawMessage(`{"seed": ` + strconv.FormatUint(seed, 10) + `}`) + //// TODO other types + //return json.RawMessage(`{"seed": ` + strconv.FormatUint(seed, 10) + `}`) + panic("TODO") default: } panic(fmt.Errorf("unexpected kind: %v", kind)) } +func randBz(rnd *rand.Rand) []byte { + n := rnd.Int31n(1024) + bz := make([]byte, n) + _, err := rnd.Read(bz) + if err != nil { + panic(err) + } + return bz +} + var testEnum = indexerbase.EnumDefinition{ Name: "TestEnum", Values: []string{"A", "B", "C"}, From 1cbe5bcda0afdc3e768dea50c12b121b5ae7b0c8 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 14:03:27 -0400 Subject: [PATCH 15/79] WIP --- indexer/base/testing/testing.go | 35 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/indexer/base/testing/testing.go b/indexer/base/testing/testing.go index 4c0812bf7aa6..83aa7a1e0983 100644 --- a/indexer/base/testing/testing.go +++ b/indexer/base/testing/testing.go @@ -13,7 +13,8 @@ import ( // two fake modules over three blocks of data. The data set should remain relatively stable between releases // and generally only be changed when new features are added, so it should be suitable for regression or golden tests. type ListenerTestFixture struct { - listener indexerbase.Listener + listener indexerbase.Listener + allKeyModule testModule } type ListenerTestFixtureOptions struct { @@ -102,22 +103,34 @@ var moduleSchemaA = indexerbase.ModuleSchema{ var maxKind = indexerbase.JSONKind -func mkTestModule() (indexerbase.ModuleSchema, func(*rand.Rand) []indexerbase.EntityUpdate) { +type testModule struct { + schema indexerbase.ModuleSchema + updater func(*rand.Rand, *indexerbase.Listener) error +} + +func mkAllKeysModule() testModule { schema := indexerbase.ModuleSchema{} for i := 1; i < int(maxKind); i++ { schema.Tables = append(schema.Tables, mkTestTable(indexerbase.Kind(i))) } - return schema, func(rnd *rand.Rand) []indexerbase.EntityUpdate { - var updates []indexerbase.EntityUpdate - for i := 1; i < int(maxKind); i++ { - // 0-10 updates per kind - n := int(rnd.Int31n(11)) - for j := 0; j < n; j++ { - updates = append(updates, mkTestUpdate(rnd, indexerbase.Kind(i))) + return testModule{ + schema: schema, + updater: func(rnd *rand.Rand, listener *indexerbase.Listener) error { + if listener.OnEntityUpdate != nil { + for i := 1; i < int(maxKind); i++ { + // 0-10 updates per kind + n := int(rnd.Int31n(11)) + for j := 0; j < n; j++ { + err := listener.OnEntityUpdate("all_keys", mkTestUpdate(rnd, indexerbase.Kind(i))) + if err != nil { + return err + } + } + } } - } - return updates + return nil + }, } } From aacab9e4a1aa1cf8a1decca6e12e54366cde838a Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 14:14:52 -0400 Subject: [PATCH 16/79] update table and entity language to object --- indexer/base/entity.go | 41 ------------------ indexer/base/enum.go | 10 +++++ indexer/base/field.go | 11 +---- indexer/base/kind.go | 2 +- indexer/base/listener.go | 12 +++--- indexer/base/module_schema.go | 4 +- .../base/{table.go => object_descriptor.go} | 12 +++--- indexer/base/object_update.go | 42 +++++++++++++++++++ indexer/base/testing/testing.go | 20 ++++----- 9 files changed, 78 insertions(+), 76 deletions(-) delete mode 100644 indexer/base/entity.go create mode 100644 indexer/base/enum.go rename indexer/base/{table.go => object_descriptor.go} (81%) create mode 100644 indexer/base/object_update.go diff --git a/indexer/base/entity.go b/indexer/base/entity.go deleted file mode 100644 index df2ee2d3dd2f..000000000000 --- a/indexer/base/entity.go +++ /dev/null @@ -1,41 +0,0 @@ -package indexerbase - -// EntityUpdate represents an update operation on an entity in the schema. -type EntityUpdate struct { - // TableName is the name of the table that the entity belongs to in the schema. - TableName string - - // Key returns the value of the primary key of the entity and must conform to these constraints with respect - // that the schema that is defined for the entity: - // - if key represents a single column, then the value must be valid for the first column in that - // column list. For instance, if there is one column in the key of type String, then the value must be of - // type string - // - if key represents multiple columns, then the value must be a slice of values where each value is valid - // for the corresponding column in the column list. For instance, if there are two columns in the key of - // type String, String, then the value must be a slice of two strings. - // If the key has no columns, meaning that this is a singleton entity, then this value is ignored and can be nil. - Key any - - // Value returns the non-primary key columns of the entity and can either conform to the same constraints - // as EntityUpdate.Key or it may be and instance of ValueUpdates. ValueUpdates can be used as a performance - // optimization to avoid copying the values of the entity into the update and/or to omit unchanged columns. - // If this is a delete operation, then this value is ignored and can be nil. - Value any - - // Delete is a flag that indicates whether this update is a delete operation. If true, then the Value field - // is ignored and can be nil. - Delete bool -} - -// ValueUpdates is an interface that represents the value columns of an entity update. Columns that -// were not updated may be excluded from the update. Consumers should be aware that implementations -// may not filter out columns that were unchanged. However, if a column is omitted from the update -// it should be considered unchanged. -type ValueUpdates interface { - - // Iterate iterates over the columns and values in the entity update. The function should return - // true to continue iteration or false to stop iteration. Each column value should conform - // to the requirements of that column's type in the schema. Iterate returns an error if - // it was unable to decode the values properly (which could be the case in lazy evaluation). - Iterate(func(col string, value any) bool) error -} diff --git a/indexer/base/enum.go b/indexer/base/enum.go new file mode 100644 index 000000000000..b1275d8a7a95 --- /dev/null +++ b/indexer/base/enum.go @@ -0,0 +1,10 @@ +package indexerbase + +// EnumDefinition represents the definition of an enum type. +type EnumDefinition struct { + // Name is the name of the enum type. + Name string + + // Values is a list of distinct values that are part of the enum type. + Values []string +} diff --git a/indexer/base/field.go b/indexer/base/field.go index 93755f47e6c4..825fd76326d3 100644 --- a/indexer/base/field.go +++ b/indexer/base/field.go @@ -1,6 +1,6 @@ package indexerbase -// Field represents a field in a table schema. +// Field represents a field in an object descriptor. type Field struct { // Name is the name of the field. Name string @@ -17,12 +17,3 @@ type Field struct { // EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind. EnumDefinition EnumDefinition } - -// EnumDefinition represents the definition of an enum type. -type EnumDefinition struct { - // Name is the name of the enum type. - Name string - - // Values is a list of distinct values that are part of the enum type. - Values []string -} diff --git a/indexer/base/kind.go b/indexer/base/kind.go index ba4046ba330b..39e1e8db3e07 100644 --- a/indexer/base/kind.go +++ b/indexer/base/kind.go @@ -1,6 +1,6 @@ package indexerbase -// Kind represents the basic type of a field in the table schema. +// Kind represents the basic type of a field in an object descriptor. // Each kind defines the types of go values which should be accepted // by listeners and generated by decoders when providing entity updates. type Kind int diff --git a/indexer/base/listener.go b/indexer/base/listener.go index 43a66f450e76..094c3218af53 100644 --- a/indexer/base/listener.go +++ b/indexer/base/listener.go @@ -8,7 +8,7 @@ import ( // It is valid for any of the methods to be nil, in which case the listener will not be called for that event. // Listeners should understand the guarantees that are provided by the source they are listening to and // understand which methods will or will not be called. For instance, most blockchains will not do logical -// decoding of data out of the box, so the InitializeModuleSchema and OnEntityUpdate methods will not be called. +// decoding of data out of the box, so the InitializeModuleSchema and OnObjectUpdate methods will not be called. // These methods will only be called when listening logical decoding is setup. type Listener struct { // Initialize is called when the listener is initialized before any other methods are called. @@ -41,18 +41,18 @@ type Listener struct { // InitializeModuleSchema should be called whenever the blockchain process starts OR whenever // logical decoding of a module is initiated. An indexer listening to this event // should ensure that they have performed whatever initialization steps (such as database - // migrations) required to receive OnEntityUpdate events for the given module. If the + // migrations) required to receive OnObjectUpdate events for the given module. If the // indexer's schema is incompatible with the module's on-chain schema, the listener should return // an error. InitializeModuleSchema func(module string, schema ModuleSchema) error - // OnEntityUpdate is called whenever an entity is updated in the module. This is only called + // OnObjectUpdate is called whenever an object is updated in a module's state. This is only called // when logical data is available. It should be assumed that the same data in raw form // is also passed to OnKVPair. - OnEntityUpdate func(module string, update EntityUpdate) error + OnObjectUpdate func(module string, update ObjectUpdate) error - // CommitCatchupSync is called after all existing entities for a module have been passed to - // OnEntityUpdate during a catch-up sync which has been initiated by return -1 for lastBlock + // CommitCatchupSync is called after all existing state for a module has been passed to + // OnObjectUpdate during a catch-up sync which has been initiated by return -1 for lastBlock // in InitializeModuleSchema. The listener should commit all the data that has been received at // this point and also save the block number as the last block that has been processed so // that processing of regular block data can resume from this point in the future. diff --git a/indexer/base/module_schema.go b/indexer/base/module_schema.go index 4e8b81c2be3c..7cf95fb04d4a 100644 --- a/indexer/base/module_schema.go +++ b/indexer/base/module_schema.go @@ -3,6 +3,6 @@ package indexerbase // ModuleSchema represents the logical schema of a module for purposes of indexing and querying. type ModuleSchema struct { - // Tables is a list of tables that are part of the schema for the module. - Tables []Table + // Objects is a list of objects that are part of the schema for the module. + Objects []ObjectDescriptor } diff --git a/indexer/base/table.go b/indexer/base/object_descriptor.go similarity index 81% rename from indexer/base/table.go rename to indexer/base/object_descriptor.go index f2061005cd9a..fecb8a8a0fa2 100644 --- a/indexer/base/table.go +++ b/indexer/base/object_descriptor.go @@ -1,16 +1,16 @@ package indexerbase -// Table represents a table in the schema of a module. -type Table struct { - // Name is the name of the table. +// ObjectDescriptor describes an object in the schema of a module. +type ObjectDescriptor struct { + // Name is the name of the object. Name string - // KeyFields is a list of fields that make up the primary key of the table. - // It can be empty in which case indexers should assume that this table is + // KeyFields is a list of fields that make up the primary key of the object. + // It can be empty in which case indexers should assume that this object is // a singleton and ony has one value. KeyFields []Field - // ValueFields is a list of fields that are not part of the primary key of the table. + // ValueFields is a list of fields that are not part of the primary key of the object. // It can be empty in the case where all fields are part of the primary key. ValueFields []Field diff --git a/indexer/base/object_update.go b/indexer/base/object_update.go new file mode 100644 index 000000000000..464cbf8c445d --- /dev/null +++ b/indexer/base/object_update.go @@ -0,0 +1,42 @@ +package indexerbase + +// ObjectUpdate represents an update operation on an object in a module's state. +type ObjectUpdate struct { + + // ObjectName is the name of the object type in the module's schema. + ObjectName string + + // Key returns the value of the primary key of the object and must conform to these constraints with respect + // that the schema that is defined for the object: + // - if key represents a single field, then the value must be valid for the first field in that + // field list. For instance, if there is one field in the key of type String, then the value must be of + // type string + // - if key represents multiple fields, then the value must be a slice of values where each value is valid + // for the corresponding field in the field list. For instance, if there are two fields in the key of + // type String, String, then the value must be a slice of two strings. + // If the key has no fields, meaning that this is a singleton object, then this value is ignored and can be nil. + Key any + + // Value returns the non-primary key fields of the object and can either conform to the same constraints + // as ObjectUpdate.Key or it may be and instance of ValueUpdates. ValueUpdates can be used as a performance + // optimization to avoid copying the values of the object into the update and/or to omit unchanged fields. + // If this is a delete operation, then this value is ignored and can be nil. + Value any + + // Delete is a flag that indicates whether this update is a delete operation. If true, then the Value field + // is ignored and can be nil. + Delete bool +} + +// ValueUpdates is an interface that represents the value fields of an object update. fields that +// were not updated may be excluded from the update. Consumers should be aware that implementations +// may not filter out fields that were unchanged. However, if a field is omitted from the update +// it should be considered unchanged. +type ValueUpdates interface { + + // Iterate iterates over the fields and values in the object update. The function should return + // true to continue iteration or false to stop iteration. Each field value should conform + // to the requirements of that field's type in the schema. Iterate returns an error if + // it was unable to decode the values properly (which could be the case in lazy evaluation). + Iterate(func(col string, value any) bool) error +} diff --git a/indexer/base/testing/testing.go b/indexer/base/testing/testing.go index 83aa7a1e0983..fcc07d13c826 100644 --- a/indexer/base/testing/testing.go +++ b/indexer/base/testing/testing.go @@ -9,7 +9,7 @@ import ( ) // ListenerTestFixture is a test fixture for testing listener implementations with a pre-defined data set -// that attempts to cover all known types of tables and fields. The test data currently includes data for +// that attempts to cover all known types of objects and fields. The test data currently includes data for // two fake modules over three blocks of data. The data set should remain relatively stable between releases // and generally only be changed when new features are added, so it should be suitable for regression or golden tests. type ListenerTestFixture struct { @@ -48,7 +48,7 @@ func (f *ListenerTestFixture) block3() error { } var moduleSchemaA = indexerbase.ModuleSchema{ - Tables: []indexerbase.Table{ + Objects: []indexerbase.ObjectDescriptor{ { "Singleton", []indexerbase.Field{}, @@ -111,18 +111,18 @@ type testModule struct { func mkAllKeysModule() testModule { schema := indexerbase.ModuleSchema{} for i := 1; i < int(maxKind); i++ { - schema.Tables = append(schema.Tables, mkTestTable(indexerbase.Kind(i))) + schema.Objects = append(schema.Objects, mkTestObjectType(indexerbase.Kind(i))) } return testModule{ schema: schema, updater: func(rnd *rand.Rand, listener *indexerbase.Listener) error { - if listener.OnEntityUpdate != nil { + if listener.OnObjectUpdate != nil { for i := 1; i < int(maxKind); i++ { // 0-10 updates per kind n := int(rnd.Int31n(11)) for j := 0; j < n; j++ { - err := listener.OnEntityUpdate("all_keys", mkTestUpdate(rnd, indexerbase.Kind(i))) + err := listener.OnObjectUpdate("all_keys", mkTestUpdate(rnd, indexerbase.Kind(i))) if err != nil { return err } @@ -134,7 +134,7 @@ func mkAllKeysModule() testModule { } } -func mkTestTable(kind indexerbase.Kind) indexerbase.Table { +func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectDescriptor { field := indexerbase.Field{ Name: fmt.Sprintf("test_%s", kind), Kind: kind, @@ -159,15 +159,15 @@ func mkTestTable(kind indexerbase.Kind) indexerbase.Table { val2Field.Name = "valNullable" val2Field.Nullable = true - return indexerbase.Table{ - Name: "KindTable", + return indexerbase.ObjectDescriptor{ + Name: fmt.Sprintf("test_%s", kind), KeyFields: []indexerbase.Field{key1Field, key2Field}, ValueFields: []indexerbase.Field{val1Field, val2Field}, } } -func mkTestUpdate(rnd *rand.Rand, kind indexerbase.Kind) indexerbase.EntityUpdate { - update := indexerbase.EntityUpdate{} +func mkTestUpdate(rnd *rand.Rand, kind indexerbase.Kind) indexerbase.ObjectUpdate { + update := indexerbase.ObjectUpdate{} k1 := mkTestValue(rnd, kind, false) k2 := mkTestValue(rnd, kind, true) From 7fd604f7582fe8f6422fc23141993e6cd0888fc9 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 14:16:53 -0400 Subject: [PATCH 17/79] update table and entity language to object --- indexer/base/entity.go | 41 ------------------ indexer/base/enum.go | 10 +++++ indexer/base/field.go | 11 +---- indexer/base/kind.go | 2 +- indexer/base/listener.go | 12 +++--- indexer/base/module_schema.go | 4 +- .../base/{table.go => object_descriptor.go} | 12 +++--- indexer/base/object_update.go | 42 +++++++++++++++++++ 8 files changed, 68 insertions(+), 66 deletions(-) delete mode 100644 indexer/base/entity.go create mode 100644 indexer/base/enum.go rename indexer/base/{table.go => object_descriptor.go} (81%) create mode 100644 indexer/base/object_update.go diff --git a/indexer/base/entity.go b/indexer/base/entity.go deleted file mode 100644 index df2ee2d3dd2f..000000000000 --- a/indexer/base/entity.go +++ /dev/null @@ -1,41 +0,0 @@ -package indexerbase - -// EntityUpdate represents an update operation on an entity in the schema. -type EntityUpdate struct { - // TableName is the name of the table that the entity belongs to in the schema. - TableName string - - // Key returns the value of the primary key of the entity and must conform to these constraints with respect - // that the schema that is defined for the entity: - // - if key represents a single column, then the value must be valid for the first column in that - // column list. For instance, if there is one column in the key of type String, then the value must be of - // type string - // - if key represents multiple columns, then the value must be a slice of values where each value is valid - // for the corresponding column in the column list. For instance, if there are two columns in the key of - // type String, String, then the value must be a slice of two strings. - // If the key has no columns, meaning that this is a singleton entity, then this value is ignored and can be nil. - Key any - - // Value returns the non-primary key columns of the entity and can either conform to the same constraints - // as EntityUpdate.Key or it may be and instance of ValueUpdates. ValueUpdates can be used as a performance - // optimization to avoid copying the values of the entity into the update and/or to omit unchanged columns. - // If this is a delete operation, then this value is ignored and can be nil. - Value any - - // Delete is a flag that indicates whether this update is a delete operation. If true, then the Value field - // is ignored and can be nil. - Delete bool -} - -// ValueUpdates is an interface that represents the value columns of an entity update. Columns that -// were not updated may be excluded from the update. Consumers should be aware that implementations -// may not filter out columns that were unchanged. However, if a column is omitted from the update -// it should be considered unchanged. -type ValueUpdates interface { - - // Iterate iterates over the columns and values in the entity update. The function should return - // true to continue iteration or false to stop iteration. Each column value should conform - // to the requirements of that column's type in the schema. Iterate returns an error if - // it was unable to decode the values properly (which could be the case in lazy evaluation). - Iterate(func(col string, value any) bool) error -} diff --git a/indexer/base/enum.go b/indexer/base/enum.go new file mode 100644 index 000000000000..b1275d8a7a95 --- /dev/null +++ b/indexer/base/enum.go @@ -0,0 +1,10 @@ +package indexerbase + +// EnumDefinition represents the definition of an enum type. +type EnumDefinition struct { + // Name is the name of the enum type. + Name string + + // Values is a list of distinct values that are part of the enum type. + Values []string +} diff --git a/indexer/base/field.go b/indexer/base/field.go index 93755f47e6c4..825fd76326d3 100644 --- a/indexer/base/field.go +++ b/indexer/base/field.go @@ -1,6 +1,6 @@ package indexerbase -// Field represents a field in a table schema. +// Field represents a field in an object descriptor. type Field struct { // Name is the name of the field. Name string @@ -17,12 +17,3 @@ type Field struct { // EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind. EnumDefinition EnumDefinition } - -// EnumDefinition represents the definition of an enum type. -type EnumDefinition struct { - // Name is the name of the enum type. - Name string - - // Values is a list of distinct values that are part of the enum type. - Values []string -} diff --git a/indexer/base/kind.go b/indexer/base/kind.go index ba4046ba330b..39e1e8db3e07 100644 --- a/indexer/base/kind.go +++ b/indexer/base/kind.go @@ -1,6 +1,6 @@ package indexerbase -// Kind represents the basic type of a field in the table schema. +// Kind represents the basic type of a field in an object descriptor. // Each kind defines the types of go values which should be accepted // by listeners and generated by decoders when providing entity updates. type Kind int diff --git a/indexer/base/listener.go b/indexer/base/listener.go index 43a66f450e76..094c3218af53 100644 --- a/indexer/base/listener.go +++ b/indexer/base/listener.go @@ -8,7 +8,7 @@ import ( // It is valid for any of the methods to be nil, in which case the listener will not be called for that event. // Listeners should understand the guarantees that are provided by the source they are listening to and // understand which methods will or will not be called. For instance, most blockchains will not do logical -// decoding of data out of the box, so the InitializeModuleSchema and OnEntityUpdate methods will not be called. +// decoding of data out of the box, so the InitializeModuleSchema and OnObjectUpdate methods will not be called. // These methods will only be called when listening logical decoding is setup. type Listener struct { // Initialize is called when the listener is initialized before any other methods are called. @@ -41,18 +41,18 @@ type Listener struct { // InitializeModuleSchema should be called whenever the blockchain process starts OR whenever // logical decoding of a module is initiated. An indexer listening to this event // should ensure that they have performed whatever initialization steps (such as database - // migrations) required to receive OnEntityUpdate events for the given module. If the + // migrations) required to receive OnObjectUpdate events for the given module. If the // indexer's schema is incompatible with the module's on-chain schema, the listener should return // an error. InitializeModuleSchema func(module string, schema ModuleSchema) error - // OnEntityUpdate is called whenever an entity is updated in the module. This is only called + // OnObjectUpdate is called whenever an object is updated in a module's state. This is only called // when logical data is available. It should be assumed that the same data in raw form // is also passed to OnKVPair. - OnEntityUpdate func(module string, update EntityUpdate) error + OnObjectUpdate func(module string, update ObjectUpdate) error - // CommitCatchupSync is called after all existing entities for a module have been passed to - // OnEntityUpdate during a catch-up sync which has been initiated by return -1 for lastBlock + // CommitCatchupSync is called after all existing state for a module has been passed to + // OnObjectUpdate during a catch-up sync which has been initiated by return -1 for lastBlock // in InitializeModuleSchema. The listener should commit all the data that has been received at // this point and also save the block number as the last block that has been processed so // that processing of regular block data can resume from this point in the future. diff --git a/indexer/base/module_schema.go b/indexer/base/module_schema.go index 4e8b81c2be3c..7cf95fb04d4a 100644 --- a/indexer/base/module_schema.go +++ b/indexer/base/module_schema.go @@ -3,6 +3,6 @@ package indexerbase // ModuleSchema represents the logical schema of a module for purposes of indexing and querying. type ModuleSchema struct { - // Tables is a list of tables that are part of the schema for the module. - Tables []Table + // Objects is a list of objects that are part of the schema for the module. + Objects []ObjectDescriptor } diff --git a/indexer/base/table.go b/indexer/base/object_descriptor.go similarity index 81% rename from indexer/base/table.go rename to indexer/base/object_descriptor.go index f2061005cd9a..fecb8a8a0fa2 100644 --- a/indexer/base/table.go +++ b/indexer/base/object_descriptor.go @@ -1,16 +1,16 @@ package indexerbase -// Table represents a table in the schema of a module. -type Table struct { - // Name is the name of the table. +// ObjectDescriptor describes an object in the schema of a module. +type ObjectDescriptor struct { + // Name is the name of the object. Name string - // KeyFields is a list of fields that make up the primary key of the table. - // It can be empty in which case indexers should assume that this table is + // KeyFields is a list of fields that make up the primary key of the object. + // It can be empty in which case indexers should assume that this object is // a singleton and ony has one value. KeyFields []Field - // ValueFields is a list of fields that are not part of the primary key of the table. + // ValueFields is a list of fields that are not part of the primary key of the object. // It can be empty in the case where all fields are part of the primary key. ValueFields []Field diff --git a/indexer/base/object_update.go b/indexer/base/object_update.go new file mode 100644 index 000000000000..464cbf8c445d --- /dev/null +++ b/indexer/base/object_update.go @@ -0,0 +1,42 @@ +package indexerbase + +// ObjectUpdate represents an update operation on an object in a module's state. +type ObjectUpdate struct { + + // ObjectName is the name of the object type in the module's schema. + ObjectName string + + // Key returns the value of the primary key of the object and must conform to these constraints with respect + // that the schema that is defined for the object: + // - if key represents a single field, then the value must be valid for the first field in that + // field list. For instance, if there is one field in the key of type String, then the value must be of + // type string + // - if key represents multiple fields, then the value must be a slice of values where each value is valid + // for the corresponding field in the field list. For instance, if there are two fields in the key of + // type String, String, then the value must be a slice of two strings. + // If the key has no fields, meaning that this is a singleton object, then this value is ignored and can be nil. + Key any + + // Value returns the non-primary key fields of the object and can either conform to the same constraints + // as ObjectUpdate.Key or it may be and instance of ValueUpdates. ValueUpdates can be used as a performance + // optimization to avoid copying the values of the object into the update and/or to omit unchanged fields. + // If this is a delete operation, then this value is ignored and can be nil. + Value any + + // Delete is a flag that indicates whether this update is a delete operation. If true, then the Value field + // is ignored and can be nil. + Delete bool +} + +// ValueUpdates is an interface that represents the value fields of an object update. fields that +// were not updated may be excluded from the update. Consumers should be aware that implementations +// may not filter out fields that were unchanged. However, if a field is omitted from the update +// it should be considered unchanged. +type ValueUpdates interface { + + // Iterate iterates over the fields and values in the object update. The function should return + // true to continue iteration or false to stop iteration. Each field value should conform + // to the requirements of that field's type in the schema. Iterate returns an error if + // it was unable to decode the values properly (which could be the case in lazy evaluation). + Iterate(func(col string, value any) bool) error +} From 4130b32cbe414e03b91ffaaa8ca44a0a3e3a1558 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 14:35:15 -0400 Subject: [PATCH 18/79] WIP --- .../base/testing/{testing.go => fixture.go} | 57 ++++++++++++++----- indexer/base/testing/fixture_test.go | 17 ++++++ indexer/base/testing/test_listener.go | 47 +++++++++++++++ 3 files changed, 106 insertions(+), 15 deletions(-) rename indexer/base/testing/{testing.go => fixture.go} (83%) create mode 100644 indexer/base/testing/fixture_test.go create mode 100644 indexer/base/testing/test_listener.go diff --git a/indexer/base/testing/testing.go b/indexer/base/testing/fixture.go similarity index 83% rename from indexer/base/testing/testing.go rename to indexer/base/testing/fixture.go index fcc07d13c826..79006542cc76 100644 --- a/indexer/base/testing/testing.go +++ b/indexer/base/testing/fixture.go @@ -1,6 +1,7 @@ package indexertesting import ( + "encoding/json" "fmt" "math/rand" "time" @@ -13,6 +14,8 @@ import ( // two fake modules over three blocks of data. The data set should remain relatively stable between releases // and generally only be changed when new features are added, so it should be suitable for regression or golden tests. type ListenerTestFixture struct { + rnd *rand.Rand + block uint64 listener indexerbase.Listener allKeyModule testModule } @@ -23,23 +26,44 @@ type ListenerTestFixtureOptions struct { func NewListenerTestFixture(listener indexerbase.Listener, options ListenerTestFixtureOptions) *ListenerTestFixture { return &ListenerTestFixture{ - listener: listener, + rnd: rand.New(rand.NewSource(0)), + listener: listener, + allKeyModule: mkAllKeysModule(), } } func (f *ListenerTestFixture) Initialize() error { + if f.listener.InitializeModuleSchema != nil { + err := f.listener.InitializeModuleSchema(f.allKeyModule.name, f.allKeyModule.schema) + if err != nil { + return err + } + } return nil } -func (f *ListenerTestFixture) NextBlock() (bool, error) { - return false, nil -} +func (f *ListenerTestFixture) NextBlock() error { + f.block++ -func (f *ListenerTestFixture) block1() error { - return nil -} + if f.listener.StartBlock != nil { + err := f.listener.StartBlock(uint64(f.block)) + if err != nil { + return err + } + } + + err := f.allKeyModule.updater(f.rnd, &f.listener) + if err != nil { + return err + } + + if f.listener.Commit != nil { + err := f.listener.Commit() + if err != nil { + return err + } + } -func (f *ListenerTestFixture) block2() error { return nil } @@ -104,6 +128,7 @@ var moduleSchemaA = indexerbase.ModuleSchema{ var maxKind = indexerbase.JSONKind type testModule struct { + name string schema indexerbase.ModuleSchema updater func(*rand.Rand, *indexerbase.Listener) error } @@ -114,7 +139,9 @@ func mkAllKeysModule() testModule { schema.Objects = append(schema.Objects, mkTestObjectType(indexerbase.Kind(i))) } + const name = "all_keys" return testModule{ + name: name, schema: schema, updater: func(rnd *rand.Rand, listener *indexerbase.Listener) error { if listener.OnObjectUpdate != nil { @@ -122,7 +149,7 @@ func mkAllKeysModule() testModule { // 0-10 updates per kind n := int(rnd.Int31n(11)) for j := 0; j < n; j++ { - err := listener.OnObjectUpdate("all_keys", mkTestUpdate(rnd, indexerbase.Kind(i))) + err := listener.OnObjectUpdate(name, mkTestUpdate(rnd, indexerbase.Kind(i))) if err != nil { return err } @@ -136,7 +163,7 @@ func mkAllKeysModule() testModule { func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectDescriptor { field := indexerbase.Field{ - Name: fmt.Sprintf("test_%s", kind), + Name: fmt.Sprintf("test_%v", kind), Kind: kind, } @@ -160,7 +187,7 @@ func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectDescriptor { val2Field.Nullable = true return indexerbase.ObjectDescriptor{ - Name: fmt.Sprintf("test_%s", kind), + Name: fmt.Sprintf("test_%v", kind), KeyFields: []indexerbase.Field{key1Field, key2Field}, ValueFields: []indexerbase.Field{val1Field, val2Field}, } @@ -232,13 +259,13 @@ func mkTestValue(rnd *rand.Rand, kind indexerbase.Kind, nullable bool) any { case indexerbase.Float64Kind: return rnd.Float64() case indexerbase.Bech32AddressKind: - panic("TODO: select from some actually valid known bech32 address strings and bytes") + // TODO: select from some actually valid known bech32 address strings and bytes" + return "cosmos1abcdefgh1234567890" case indexerbase.EnumKind: return testEnum.Values[rnd.Int31n(int32(len(testEnum.Values)))] case indexerbase.JSONKind: - //// TODO other types - //return json.RawMessage(`{"seed": ` + strconv.FormatUint(seed, 10) + `}`) - panic("TODO") + // TODO: other types + return json.RawMessage(`{}`) default: } panic(fmt.Errorf("unexpected kind: %v", kind)) diff --git a/indexer/base/testing/fixture_test.go b/indexer/base/testing/fixture_test.go new file mode 100644 index 000000000000..7204bda14767 --- /dev/null +++ b/indexer/base/testing/fixture_test.go @@ -0,0 +1,17 @@ +package indexertesting + +import "testing" + +func TestListenerFixture(t *testing.T) { + fixture := NewListenerTestFixture(StdoutListener(), ListenerTestFixtureOptions{}) + + err := fixture.Initialize() + if err != nil { + t.Fatal(err) + } + + err = fixture.NextBlock() + if err != nil { + t.Fatal(err) + } +} diff --git a/indexer/base/testing/test_listener.go b/indexer/base/testing/test_listener.go new file mode 100644 index 000000000000..4b2d186d97ad --- /dev/null +++ b/indexer/base/testing/test_listener.go @@ -0,0 +1,47 @@ +package indexertesting + +import ( + "fmt" + "io" + "os" + + indexerbase "cosmossdk.io/indexer/base" +) + +func StdoutListener() indexerbase.Listener { + return WriterListener(os.Stdout) +} + +func WriterListener(w io.Writer) indexerbase.Listener { + return indexerbase.Listener{ + Initialize: func(data indexerbase.InitializationData) (lastBlockPersisted int64, err error) { + _, err = fmt.Fprintf(w, "Initialize: %v\n", data) + return 0, err + }, + StartBlock: func(u uint64) error { + _, err := fmt.Fprintf(w, "StartBlock: %d\n", u) + return err + }, + OnBlockHeader: func(data indexerbase.BlockHeaderData) error { + _, err := fmt.Fprintf(w, "OnBlockHeader: %v\n", data) + return err + }, + OnTx: nil, + OnEvent: nil, + OnKVPair: nil, + Commit: func() error { + _, err := fmt.Fprintf(w, "Commit\n") + return err + }, + InitializeModuleSchema: func(moduleName string, schema indexerbase.ModuleSchema) error { + _, err := fmt.Fprintf(w, "InitializeModuleSchema: %s %v\n", moduleName, schema) + return err + }, + OnObjectUpdate: func(moduleName string, data indexerbase.ObjectUpdate) error { + //_, err := fmt.Fprintf(w, "OnObjectUpdate: %s: %v\n", moduleName, data) + //return err + return nil + }, + CommitCatchupSync: nil, + } +} From e840496bec5c15b455fee663e8fdb2d181afe644 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 14:37:12 -0400 Subject: [PATCH 19/79] rename to type --- indexer/base/field.go | 2 +- indexer/base/kind.go | 2 +- indexer/base/module_schema.go | 4 ++-- indexer/base/{object_descriptor.go => object_type.go} | 4 ++-- indexer/base/object_update.go | 4 ++-- indexer/base/testing/fixture.go | 8 ++++---- 6 files changed, 12 insertions(+), 12 deletions(-) rename indexer/base/{object_descriptor.go => object_type.go} (90%) diff --git a/indexer/base/field.go b/indexer/base/field.go index 825fd76326d3..713abdeb4c86 100644 --- a/indexer/base/field.go +++ b/indexer/base/field.go @@ -1,6 +1,6 @@ package indexerbase -// Field represents a field in an object descriptor. +// Field represents a field in an object type. type Field struct { // Name is the name of the field. Name string diff --git a/indexer/base/kind.go b/indexer/base/kind.go index 39e1e8db3e07..064a6543beaf 100644 --- a/indexer/base/kind.go +++ b/indexer/base/kind.go @@ -1,6 +1,6 @@ package indexerbase -// Kind represents the basic type of a field in an object descriptor. +// Kind represents the basic type of a field in an object. // Each kind defines the types of go values which should be accepted // by listeners and generated by decoders when providing entity updates. type Kind int diff --git a/indexer/base/module_schema.go b/indexer/base/module_schema.go index 7cf95fb04d4a..45da535e87e3 100644 --- a/indexer/base/module_schema.go +++ b/indexer/base/module_schema.go @@ -3,6 +3,6 @@ package indexerbase // ModuleSchema represents the logical schema of a module for purposes of indexing and querying. type ModuleSchema struct { - // Objects is a list of objects that are part of the schema for the module. - Objects []ObjectDescriptor + // ObjectTypes describe the types of objects that are part of the module's schema. + ObjectTypes []ObjectType } diff --git a/indexer/base/object_descriptor.go b/indexer/base/object_type.go similarity index 90% rename from indexer/base/object_descriptor.go rename to indexer/base/object_type.go index fecb8a8a0fa2..06f8adbc92a7 100644 --- a/indexer/base/object_descriptor.go +++ b/indexer/base/object_type.go @@ -1,7 +1,7 @@ package indexerbase -// ObjectDescriptor describes an object in the schema of a module. -type ObjectDescriptor struct { +// ObjectType describes an object type a module schema. +type ObjectType struct { // Name is the name of the object. Name string diff --git a/indexer/base/object_update.go b/indexer/base/object_update.go index 464cbf8c445d..fa3d37c8e71b 100644 --- a/indexer/base/object_update.go +++ b/indexer/base/object_update.go @@ -3,8 +3,8 @@ package indexerbase // ObjectUpdate represents an update operation on an object in a module's state. type ObjectUpdate struct { - // ObjectName is the name of the object type in the module's schema. - ObjectName string + // TypeName is the name of the object type in the module's schema. + TypeName string // Key returns the value of the primary key of the object and must conform to these constraints with respect // that the schema that is defined for the object: diff --git a/indexer/base/testing/fixture.go b/indexer/base/testing/fixture.go index 79006542cc76..af6ae8cfe940 100644 --- a/indexer/base/testing/fixture.go +++ b/indexer/base/testing/fixture.go @@ -72,7 +72,7 @@ func (f *ListenerTestFixture) block3() error { } var moduleSchemaA = indexerbase.ModuleSchema{ - Objects: []indexerbase.ObjectDescriptor{ + ObjectTypes: []indexerbase.ObjectType{ { "Singleton", []indexerbase.Field{}, @@ -136,7 +136,7 @@ type testModule struct { func mkAllKeysModule() testModule { schema := indexerbase.ModuleSchema{} for i := 1; i < int(maxKind); i++ { - schema.Objects = append(schema.Objects, mkTestObjectType(indexerbase.Kind(i))) + schema.ObjectTypes = append(schema.ObjectTypes, mkTestObjectType(indexerbase.Kind(i))) } const name = "all_keys" @@ -161,7 +161,7 @@ func mkAllKeysModule() testModule { } } -func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectDescriptor { +func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectType { field := indexerbase.Field{ Name: fmt.Sprintf("test_%v", kind), Kind: kind, @@ -186,7 +186,7 @@ func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectDescriptor { val2Field.Name = "valNullable" val2Field.Nullable = true - return indexerbase.ObjectDescriptor{ + return indexerbase.ObjectType{ Name: fmt.Sprintf("test_%v", kind), KeyFields: []indexerbase.Field{key1Field, key2Field}, ValueFields: []indexerbase.Field{val1Field, val2Field}, From 4a00094d141bd9eaaccd3b4d68d1adc10b8d691b Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 14:37:53 -0400 Subject: [PATCH 20/79] rename to type --- indexer/base/field.go | 2 +- indexer/base/kind.go | 2 +- indexer/base/module_schema.go | 4 ++-- indexer/base/{object_descriptor.go => object_type.go} | 4 ++-- indexer/base/object_update.go | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename indexer/base/{object_descriptor.go => object_type.go} (90%) diff --git a/indexer/base/field.go b/indexer/base/field.go index 825fd76326d3..713abdeb4c86 100644 --- a/indexer/base/field.go +++ b/indexer/base/field.go @@ -1,6 +1,6 @@ package indexerbase -// Field represents a field in an object descriptor. +// Field represents a field in an object type. type Field struct { // Name is the name of the field. Name string diff --git a/indexer/base/kind.go b/indexer/base/kind.go index 39e1e8db3e07..064a6543beaf 100644 --- a/indexer/base/kind.go +++ b/indexer/base/kind.go @@ -1,6 +1,6 @@ package indexerbase -// Kind represents the basic type of a field in an object descriptor. +// Kind represents the basic type of a field in an object. // Each kind defines the types of go values which should be accepted // by listeners and generated by decoders when providing entity updates. type Kind int diff --git a/indexer/base/module_schema.go b/indexer/base/module_schema.go index 7cf95fb04d4a..45da535e87e3 100644 --- a/indexer/base/module_schema.go +++ b/indexer/base/module_schema.go @@ -3,6 +3,6 @@ package indexerbase // ModuleSchema represents the logical schema of a module for purposes of indexing and querying. type ModuleSchema struct { - // Objects is a list of objects that are part of the schema for the module. - Objects []ObjectDescriptor + // ObjectTypes describe the types of objects that are part of the module's schema. + ObjectTypes []ObjectType } diff --git a/indexer/base/object_descriptor.go b/indexer/base/object_type.go similarity index 90% rename from indexer/base/object_descriptor.go rename to indexer/base/object_type.go index fecb8a8a0fa2..06f8adbc92a7 100644 --- a/indexer/base/object_descriptor.go +++ b/indexer/base/object_type.go @@ -1,7 +1,7 @@ package indexerbase -// ObjectDescriptor describes an object in the schema of a module. -type ObjectDescriptor struct { +// ObjectType describes an object type a module schema. +type ObjectType struct { // Name is the name of the object. Name string diff --git a/indexer/base/object_update.go b/indexer/base/object_update.go index 464cbf8c445d..fa3d37c8e71b 100644 --- a/indexer/base/object_update.go +++ b/indexer/base/object_update.go @@ -3,8 +3,8 @@ package indexerbase // ObjectUpdate represents an update operation on an object in a module's state. type ObjectUpdate struct { - // ObjectName is the name of the object type in the module's schema. - ObjectName string + // TypeName is the name of the object type in the module's schema. + TypeName string // Key returns the value of the primary key of the object and must conform to these constraints with respect // that the schema that is defined for the object: From e44dea2176916a9494c5a31a4591b06eeeda5c45 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 14:49:23 -0400 Subject: [PATCH 21/79] spin out into its own go.mod --- indexer/testing/CHANGELOG.md | 37 +++++++++++++++++++++ indexer/testing/README.md | 4 +++ indexer/{base => }/testing/fixture.go | 2 +- indexer/{base => }/testing/fixture_test.go | 0 indexer/testing/go.mod | 9 +++++ indexer/{base => }/testing/test_listener.go | 0 6 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 indexer/testing/CHANGELOG.md create mode 100644 indexer/testing/README.md rename indexer/{base => }/testing/fixture.go (99%) rename indexer/{base => }/testing/fixture_test.go (100%) create mode 100644 indexer/testing/go.mod rename indexer/{base => }/testing/test_listener.go (100%) diff --git a/indexer/testing/CHANGELOG.md b/indexer/testing/CHANGELOG.md new file mode 100644 index 000000000000..0c3c9d03857f --- /dev/null +++ b/indexer/testing/CHANGELOG.md @@ -0,0 +1,37 @@ + + +# Changelog + +## [Unreleased] diff --git a/indexer/testing/README.md b/indexer/testing/README.md new file mode 100644 index 000000000000..91921cac8c14 --- /dev/null +++ b/indexer/testing/README.md @@ -0,0 +1,4 @@ +# Indexer Testing + +This module contains core test utilities and fixtures for testing indexer functionality. It is managed as a separate go module to manage versions better and allow for dependencies on useful testing libraries without imposing those +elsewhere. \ No newline at end of file diff --git a/indexer/base/testing/fixture.go b/indexer/testing/fixture.go similarity index 99% rename from indexer/base/testing/fixture.go rename to indexer/testing/fixture.go index af6ae8cfe940..4d8e30e061cd 100644 --- a/indexer/base/testing/fixture.go +++ b/indexer/testing/fixture.go @@ -26,7 +26,7 @@ type ListenerTestFixtureOptions struct { func NewListenerTestFixture(listener indexerbase.Listener, options ListenerTestFixtureOptions) *ListenerTestFixture { return &ListenerTestFixture{ - rnd: rand.New(rand.NewSource(0)), + rnd: rand.New(rand.NewSource(1)), listener: listener, allKeyModule: mkAllKeysModule(), } diff --git a/indexer/base/testing/fixture_test.go b/indexer/testing/fixture_test.go similarity index 100% rename from indexer/base/testing/fixture_test.go rename to indexer/testing/fixture_test.go diff --git a/indexer/testing/go.mod b/indexer/testing/go.mod new file mode 100644 index 000000000000..ad7061c26963 --- /dev/null +++ b/indexer/testing/go.mod @@ -0,0 +1,9 @@ +module cosmossdk.io/indexer/testing + +require ( + cosmossdk.io/indexer/base v0.0.0 +) + +replace cosmossdk.io/indexer/base => ../base + +go 1.22 diff --git a/indexer/base/testing/test_listener.go b/indexer/testing/test_listener.go similarity index 100% rename from indexer/base/testing/test_listener.go rename to indexer/testing/test_listener.go From 0c7f52977a2fb596f7eca12ea98dc52f517b3775 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 14:50:35 -0400 Subject: [PATCH 22/79] add CHANGELOG.md --- indexer/base/CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 indexer/base/CHANGELOG.md diff --git a/indexer/base/CHANGELOG.md b/indexer/base/CHANGELOG.md new file mode 100644 index 000000000000..0c3c9d03857f --- /dev/null +++ b/indexer/base/CHANGELOG.md @@ -0,0 +1,37 @@ + + +# Changelog + +## [Unreleased] From a94dd4795a1f2bc5e49edc0c74f604f31fe75034 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 15:12:56 -0400 Subject: [PATCH 23/79] better faking --- indexer/testing/fixture.go | 86 +++++++++++++++++--------------- indexer/testing/go.mod | 6 +-- indexer/testing/go.sum | 2 + indexer/testing/test_listener.go | 5 +- 4 files changed, 54 insertions(+), 45 deletions(-) create mode 100644 indexer/testing/go.sum diff --git a/indexer/testing/fixture.go b/indexer/testing/fixture.go index 4d8e30e061cd..1758039c416b 100644 --- a/indexer/testing/fixture.go +++ b/indexer/testing/fixture.go @@ -3,9 +3,11 @@ package indexertesting import ( "encoding/json" "fmt" - "math/rand" + rand "math/rand/v2" "time" + "github.com/brianvoe/gofakeit/v7" + indexerbase "cosmossdk.io/indexer/base" ) @@ -14,7 +16,7 @@ import ( // two fake modules over three blocks of data. The data set should remain relatively stable between releases // and generally only be changed when new features are added, so it should be suitable for regression or golden tests. type ListenerTestFixture struct { - rnd *rand.Rand + rndSource rand.Source block uint64 listener indexerbase.Listener allKeyModule testModule @@ -26,7 +28,7 @@ type ListenerTestFixtureOptions struct { func NewListenerTestFixture(listener indexerbase.Listener, options ListenerTestFixtureOptions) *ListenerTestFixture { return &ListenerTestFixture{ - rnd: rand.New(rand.NewSource(1)), + rndSource: rand.NewPCG(1, 2), listener: listener, allKeyModule: mkAllKeysModule(), } @@ -52,7 +54,7 @@ func (f *ListenerTestFixture) NextBlock() error { } } - err := f.allKeyModule.updater(f.rnd, &f.listener) + err := f.allKeyModule.updater(f.rndSource, &f.listener) if err != nil { return err } @@ -130,7 +132,7 @@ var maxKind = indexerbase.JSONKind type testModule struct { name string schema indexerbase.ModuleSchema - updater func(*rand.Rand, *indexerbase.Listener) error + updater func(rand.Source, *indexerbase.Listener) error } func mkAllKeysModule() testModule { @@ -143,13 +145,14 @@ func mkAllKeysModule() testModule { return testModule{ name: name, schema: schema, - updater: func(rnd *rand.Rand, listener *indexerbase.Listener) error { + updater: func(source rand.Source, listener *indexerbase.Listener) error { if listener.OnObjectUpdate != nil { for i := 1; i < int(maxKind); i++ { + rnd := rand.New(source) // 0-10 updates per kind - n := int(rnd.Int31n(11)) + n := int(rnd.Uint32N(11)) for j := 0; j < n; j++ { - err := listener.OnObjectUpdate(name, mkTestUpdate(rnd, indexerbase.Kind(i))) + err := listener.OnObjectUpdate(name, mkTestUpdate(source, indexerbase.Kind(i))) if err != nil { return err } @@ -193,15 +196,15 @@ func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectType { } } -func mkTestUpdate(rnd *rand.Rand, kind indexerbase.Kind) indexerbase.ObjectUpdate { +func mkTestUpdate(rnd rand.Source, kind indexerbase.Kind) indexerbase.ObjectUpdate { update := indexerbase.ObjectUpdate{} k1 := mkTestValue(rnd, kind, false) k2 := mkTestValue(rnd, kind, true) update.Key = []any{k1, k2} - // delete 10% of the time - if rnd.Int31n(10) == 1 { + // delete 50% of the time + if rnd.Uint64()%2 == 0 { update.Delete = true return update } @@ -213,70 +216,75 @@ func mkTestUpdate(rnd *rand.Rand, kind indexerbase.Kind) indexerbase.ObjectUpdat return update } -func mkTestValue(rnd *rand.Rand, kind indexerbase.Kind, nullable bool) any { - // if it's nullable, return nil 10% of the time - if nullable && rnd.Int31n(10) == 1 { +func mkTestValue(src rand.Source, kind indexerbase.Kind, nullable bool) any { + faker := gofakeit.NewFaker(src, false) + // if it's nullable, return nil 50% of the time + if nullable && faker.Bool() { return nil } switch kind { case indexerbase.StringKind: // TODO fmt.Stringer - return string(randBz(rnd)) + return faker.LoremIpsumSentence(faker.IntN(100)) case indexerbase.BytesKind: - return randBz(rnd) + return randBytes(src) case indexerbase.Int8Kind: - return int8(rnd.Int31n(256) - 128) + return faker.Int8() case indexerbase.Int16Kind: - return int16(rnd.Int31n(65536) - 32768) + return faker.Int16() case indexerbase.Uint8Kind: - return uint8(rnd.Int31n(256)) + return faker.Uint16() case indexerbase.Uint16Kind: - return uint16(rnd.Int31n(65536)) + return faker.Uint16() case indexerbase.Int32Kind: - return int32(rnd.Int63n(4294967296) - 2147483648) + return faker.Int32() case indexerbase.Uint32Kind: - return uint32(rnd.Int63n(4294967296)) + return faker.Uint32() case indexerbase.Int64Kind: - return rnd.Int63() + return faker.Int64() case indexerbase.Uint64Kind: - return rnd.Uint64() + return faker.Uint64() case indexerbase.IntegerKind: - x := rnd.Int63() + x := faker.Int64() return fmt.Sprintf("%d", x) case indexerbase.DecimalKind: - x := rnd.Int63() - y := rnd.Int63n(1000000000) + x := faker.Int64() + y := faker.UintN(1000000) return fmt.Sprintf("%d.%d", x, y) case indexerbase.BoolKind: - return rnd.Int31n(2) == 1 + return faker.Bool() case indexerbase.TimeKind: - return time.Unix(rnd.Int63(), rnd.Int63n(1000000000)) + return time.Unix(faker.Int64(), int64(faker.UintN(1000000000))) case indexerbase.DurationKind: - return time.Duration(rnd.Int63()) + return time.Duration(faker.Int64()) case indexerbase.Float32Kind: - return float32(rnd.Float64()) + return faker.Float32() case indexerbase.Float64Kind: - return rnd.Float64() + return faker.Float64() case indexerbase.Bech32AddressKind: // TODO: select from some actually valid known bech32 address strings and bytes" return "cosmos1abcdefgh1234567890" case indexerbase.EnumKind: - return testEnum.Values[rnd.Int31n(int32(len(testEnum.Values)))] + return faker.RandomString(testEnum.Values) case indexerbase.JSONKind: // TODO: other types - return json.RawMessage(`{}`) + bz, err := faker.JSON(nil) + if err != nil { + panic(err) + } + return json.RawMessage(bz) default: } panic(fmt.Errorf("unexpected kind: %v", kind)) } -func randBz(rnd *rand.Rand) []byte { - n := rnd.Int31n(1024) +func randBytes(src rand.Source) []byte { + rnd := rand.New(src) + n := rnd.IntN(1024) bz := make([]byte, n) - _, err := rnd.Read(bz) - if err != nil { - panic(err) + for i := 0; i < n; i++ { + bz[i] = byte(rnd.Uint32N(256)) } return bz } diff --git a/indexer/testing/go.mod b/indexer/testing/go.mod index ad7061c26963..231dac91f964 100644 --- a/indexer/testing/go.mod +++ b/indexer/testing/go.mod @@ -1,8 +1,8 @@ module cosmossdk.io/indexer/testing -require ( - cosmossdk.io/indexer/base v0.0.0 -) +require cosmossdk.io/indexer/base v0.0.0 + +require github.com/brianvoe/gofakeit/v7 v7.0.3 // indirect replace cosmossdk.io/indexer/base => ../base diff --git a/indexer/testing/go.sum b/indexer/testing/go.sum new file mode 100644 index 000000000000..9bf55d041413 --- /dev/null +++ b/indexer/testing/go.sum @@ -0,0 +1,2 @@ +github.com/brianvoe/gofakeit/v7 v7.0.3 h1:tGCt+eYfhTMWE1ko5G2EO1f/yE44yNpIwUb4h32O0wo= +github.com/brianvoe/gofakeit/v7 v7.0.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= diff --git a/indexer/testing/test_listener.go b/indexer/testing/test_listener.go index 4b2d186d97ad..ea0998098529 100644 --- a/indexer/testing/test_listener.go +++ b/indexer/testing/test_listener.go @@ -38,9 +38,8 @@ func WriterListener(w io.Writer) indexerbase.Listener { return err }, OnObjectUpdate: func(moduleName string, data indexerbase.ObjectUpdate) error { - //_, err := fmt.Fprintf(w, "OnObjectUpdate: %s: %v\n", moduleName, data) - //return err - return nil + _, err := fmt.Fprintf(w, "OnObjectUpdate: %s: %v\n", moduleName, data) + return err }, CommitCatchupSync: nil, } From 7990487238a93c0b574cbad5133d6d2ad11fdf97 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 17:50:24 -0400 Subject: [PATCH 24/79] WIP --- indexer/testing/fixture.go | 77 ++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/indexer/testing/fixture.go b/indexer/testing/fixture.go index 1758039c416b..0550ac9b2646 100644 --- a/indexer/testing/fixture.go +++ b/indexer/testing/fixture.go @@ -48,7 +48,7 @@ func (f *ListenerTestFixture) NextBlock() error { f.block++ if f.listener.StartBlock != nil { - err := f.listener.StartBlock(uint64(f.block)) + err := f.listener.StartBlock(f.block) if err != nil { return err } @@ -130,37 +130,68 @@ var moduleSchemaA = indexerbase.ModuleSchema{ var maxKind = indexerbase.JSONKind type testModule struct { - name string - schema indexerbase.ModuleSchema + name string + schema indexerbase.ModuleSchema + state map[string]*testObjectStore +} + +type testObjectStore struct { updater func(rand.Source, *indexerbase.Listener) error + state map[any]any +} + +type value struct { + value any + state valueState } -func mkAllKeysModule() testModule { - schema := indexerbase.ModuleSchema{} +type valueState int + +const ( + valueStateNotInitialized valueState = iota + valueStateSet + valueStateDeleted +) + +func mkAllKeysModule(src rand.Source) *testModule { + mod := &testModule{} for i := 1; i < int(maxKind); i++ { - schema.ObjectTypes = append(schema.ObjectTypes, mkTestObjectType(indexerbase.Kind(i))) + kind := indexerbase.Kind(i) + typ := mkTestObjectType(kind) + mod.schema.ObjectTypes = append(mod.schema.ObjectTypes, typ) + state := map[any]any{} + // generate 5 keys + for j := 0; j < 5; j++ { + key1 := mkTestValue(src, kind, false) + key2 := mkTestValue(src, kind, true) + key := []any{key1, key2} + state[key] = nil // initialize as nil + } + + objStore := &testObjectStore{ + state: state, + } + mod.state[typ.Name] = objStore } - const name = "all_keys" + const name = "all_kinds" return testModule{ name: name, schema: schema, - updater: func(source rand.Source, listener *indexerbase.Listener) error { - if listener.OnObjectUpdate != nil { - for i := 1; i < int(maxKind); i++ { - rnd := rand.New(source) - // 0-10 updates per kind - n := int(rnd.Uint32N(11)) - for j := 0; j < n; j++ { - err := listener.OnObjectUpdate(name, mkTestUpdate(source, indexerbase.Kind(i))) - if err != nil { - return err - } - } - } - } - return nil - }, + //updater: func(source rand.Source, listener *indexerbase.Listener) error { + // if listener.OnObjectUpdate != nil { + // for i := 1; i < int(maxKind); i++ { + // // two updates per kind + // for j := 0; j < 2; j++ { + // err := listener.OnObjectUpdate(name, mkTestUpdate(source, indexerbase.Kind(i))) + // if err != nil { + // return err + // } + // } + // } + // } + // return nil + //}, } } From 408ddc465115851c7d8e05507558b9ba045e4136 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 13 Jun 2024 18:44:14 -0400 Subject: [PATCH 25/79] add DecodableModule interface --- indexer/base/decoder.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 indexer/base/decoder.go diff --git a/indexer/base/decoder.go b/indexer/base/decoder.go new file mode 100644 index 000000000000..fb745c2ac6fe --- /dev/null +++ b/indexer/base/decoder.go @@ -0,0 +1,27 @@ +package indexerbase + +// DecodableModule is an interface that modules can implement to provide a ModuleDecoder. +// Usually these modules would also implement appmodule.AppModule, but that is not included +// to keep this package free of any dependencies. +type DecodableModule interface { + + // ModuleDecoder returns a ModuleDecoder for the module. + ModuleDecoder() (ModuleDecoder, error) +} + +// ModuleDecoder is a struct that contains the schema and a KVDecoder for a module. +type ModuleDecoder struct { + // Schema is the schema for the module. + Schema ModuleSchema + + // KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. + // If modules pass logical updates directly to the engine and don't require logical decoding of raw bytes, + // then this function should be nil. + KVDecoder KVDecoder +} + +// KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. +// If the KV-pair doesn't represent an object update, the function should return false +// as the second return value. Error should only be non-nil when the decoder expected +// to parse a valid update and was unable to. +type KVDecoder = func(key, value []byte) (ObjectUpdate, bool, error) From bc98756c26780181e9e737a7fe74c1b591b388c2 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 14 Jun 2024 14:21:07 -0400 Subject: [PATCH 26/79] make compatible with go 1.12 --- indexer/base/go.mod | 7 ++++--- indexer/base/object_update.go | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/indexer/base/go.mod b/indexer/base/go.mod index c369648761e8..a373d4dc8377 100644 --- a/indexer/base/go.mod +++ b/indexer/base/go.mod @@ -1,6 +1,7 @@ module cosmossdk.io/indexer/base -// NOTE: this go.mod should have zero dependencies and remain on an older version of Go -// to be compatible with legacy codebases. +// NOTE: this go.mod should have zero dependencies and remain on go 1.12 to stay compatible +// with all known production releases of the Cosmos SDK. This is to ensure that all historical +// apps could be patched to support indexing if desired. -go 1.19 +go 1.12 diff --git a/indexer/base/object_update.go b/indexer/base/object_update.go index fa3d37c8e71b..e99d625404fb 100644 --- a/indexer/base/object_update.go +++ b/indexer/base/object_update.go @@ -15,13 +15,13 @@ type ObjectUpdate struct { // for the corresponding field in the field list. For instance, if there are two fields in the key of // type String, String, then the value must be a slice of two strings. // If the key has no fields, meaning that this is a singleton object, then this value is ignored and can be nil. - Key any + Key interface{} // Value returns the non-primary key fields of the object and can either conform to the same constraints // as ObjectUpdate.Key or it may be and instance of ValueUpdates. ValueUpdates can be used as a performance // optimization to avoid copying the values of the object into the update and/or to omit unchanged fields. // If this is a delete operation, then this value is ignored and can be nil. - Value any + Value interface{} // Delete is a flag that indicates whether this update is a delete operation. If true, then the Value field // is ignored and can be nil. @@ -38,5 +38,5 @@ type ValueUpdates interface { // true to continue iteration or false to stop iteration. Each field value should conform // to the requirements of that field's type in the schema. Iterate returns an error if // it was unable to decode the values properly (which could be the case in lazy evaluation). - Iterate(func(col string, value any) bool) error + Iterate(func(col string, value interface{}) bool) error } From 68d0afc5ac85399f09d6b983499772dd5cadaf8f Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 17 Jun 2024 08:58:09 -0400 Subject: [PATCH 27/79] remove CommitCatchupSync - catch-up design in flux, may be premature to specify this --- indexer/base/listener.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/indexer/base/listener.go b/indexer/base/listener.go index 094c3218af53..488ed87d6bbc 100644 --- a/indexer/base/listener.go +++ b/indexer/base/listener.go @@ -50,13 +50,6 @@ type Listener struct { // when logical data is available. It should be assumed that the same data in raw form // is also passed to OnKVPair. OnObjectUpdate func(module string, update ObjectUpdate) error - - // CommitCatchupSync is called after all existing state for a module has been passed to - // OnObjectUpdate during a catch-up sync which has been initiated by return -1 for lastBlock - // in InitializeModuleSchema. The listener should commit all the data that has been received at - // this point and also save the block number as the last block that has been processed so - // that processing of regular block data can resume from this point in the future. - CommitCatchupSync func(module string, block uint64) error } // InitializationData represents initialization data that is passed to a listener. From f3a4832caac139a90a4e70c74c5911d7233bdbde Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 17 Jun 2024 10:24:40 -0400 Subject: [PATCH 28/79] create table --- indexer/postgres/col_type.go | 48 +++++++++++++++++++ indexer/postgres/create_table.go | 79 ++++++++++++++++++++++++++++++++ indexer/postgres/gen.go | 15 ++++++ indexer/postgres/go.mod | 20 ++++---- indexer/postgres/indexer.go | 72 ----------------------------- indexer/postgres/meta_schema.sql | 5 -- 6 files changed, 151 insertions(+), 88 deletions(-) create mode 100644 indexer/postgres/col_type.go create mode 100644 indexer/postgres/create_table.go create mode 100644 indexer/postgres/gen.go delete mode 100644 indexer/postgres/indexer.go delete mode 100644 indexer/postgres/meta_schema.sql diff --git a/indexer/postgres/col_type.go b/indexer/postgres/col_type.go new file mode 100644 index 000000000000..0365226e6c69 --- /dev/null +++ b/indexer/postgres/col_type.go @@ -0,0 +1,48 @@ +package postgres + +import ( + "fmt" + + indexerbase "cosmossdk.io/indexer/base" +) + +func columnType(field indexerbase.Field) (string, error) { + switch field.Kind { + case indexerbase.StringKind: + return "TEXT", nil + case indexerbase.BoolKind: + return "BOOLEAN", nil + case indexerbase.BytesKind: + return "BYTEA", nil + case indexerbase.Int8Kind: + return "SMALLINT", nil + case indexerbase.Int16Kind: + return "SMALLINT", nil + case indexerbase.Int32Kind: + return "INTEGER", nil + case indexerbase.Int64Kind: + return "BIGINT", nil + case indexerbase.Uint8Kind: + return "SMALLINT", nil + case indexerbase.Uint16Kind: + return "INTEGER", nil + case indexerbase.Uint32Kind: + return "BIGINT", nil + case indexerbase.Uint64Kind: + return "NUMERIC", nil + case indexerbase.DecimalKind: + return "NUMERIC", nil + case indexerbase.Float32Kind: + return "REAL", nil + case indexerbase.Float64Kind: + return "DOUBLE PRECISION", nil + case indexerbase.EnumKind: + return "TEXT", fmt.Errorf("enums not supported yet") + case indexerbase.JSONKind: + return "JSONB", nil + case indexerbase.Bech32AddressKind: + return "TEXT", nil + default: + return "", fmt.Errorf("unsupported kind %v for field %s", field.Kind, field.Name) + } +} diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go new file mode 100644 index 000000000000..7c31d83d19a9 --- /dev/null +++ b/indexer/postgres/create_table.go @@ -0,0 +1,79 @@ +package postgres + +import ( + "fmt" + "io" + "strings" + + indexerbase "cosmossdk.io/indexer/base" +) + +// CreateTable generates a CREATE TABLE statement for the object type. +func (s *SQLGenerator) CreateTable(writer io.Writer) error { + _, err := fmt.Fprintf(writer, "CREATE TABLE IF NOT EXISTS %s (\n\t", s.typ.Name) + if err != nil { + return err + } + isSingleton := false + if len(s.typ.KeyFields) == 0 { + isSingleton = true + _, err = fmt.Fprintf(writer, "_id INTEGER NOT NULL CHECK (_id = 1),\n\t") + } else { + for _, field := range s.typ.KeyFields { + err = s.createColumnDef(writer, field) + if err != nil { + return err + } + } + } + + for _, field := range s.typ.ValueFields { + err = s.createColumnDef(writer, field) + if err != nil { + return err + } + } + + var pKeys []string + if !isSingleton { + for _, field := range s.typ.KeyFields { + pKeys = append(pKeys, field.Name) + } + } else { + pKeys = []string{"_id"} + } + + _, err = fmt.Fprintf(writer, "PRIMARY KEY (%s)\n", strings.Join(pKeys, ", ")) + if err != nil { + return err + } + + _, err = fmt.Fprintf(writer, ");") + if err != nil { + return err + } + + return nil +} + +func (s *SQLGenerator) createColumnDef(writer io.Writer, field indexerbase.Field) error { + typeStr, err := columnType(field) + if err != nil { + return err + } + + _, err = fmt.Fprintf(writer, "%s %s", field.Name, typeStr) + if err != nil { + return err + } + + if !field.Nullable { + _, err = fmt.Fprint(writer, " NOT") + if err != nil { + return err + } + } + + _, err = fmt.Fprint(writer, " NULL,\\n\\t") + return err +} diff --git a/indexer/postgres/gen.go b/indexer/postgres/gen.go new file mode 100644 index 000000000000..664d0051ad86 --- /dev/null +++ b/indexer/postgres/gen.go @@ -0,0 +1,15 @@ +package postgres + +import ( + indexerbase "cosmossdk.io/indexer/base" +) + +// SQLGenerator is a helper struct that generates SQL for a given object type. +type SQLGenerator struct { + typ indexerbase.ObjectType +} + +// NewSQLGenerator creates a new SQLGenerator for the given object type. +func NewSQLGenerator(typ indexerbase.ObjectType) *SQLGenerator { + return &SQLGenerator{typ: typ} +} diff --git a/indexer/postgres/go.mod b/indexer/postgres/go.mod index 2bfab0f3010d..353f37046e2f 100644 --- a/indexer/postgres/go.mod +++ b/indexer/postgres/go.mod @@ -1,17 +1,15 @@ module cosmossdk.io/indexer/postgres -require ( - cosmossdk.io/indexer/base v0.0.0-00010101000000-000000000000 - github.com/jackc/pgx/v5 v5.6.0 -) +// NOTE: we are staying on an earlier version of golang to avoid problems building +// with older codebases. +go 1.12 require ( - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect + // NOTE: cosmossdk.io/indexer/base should be the only dependency here + // so there are no problems building this with any version of the SDK. + // This module should only use the golang standard library (database/sql) + // and cosmossdk.io/indexer/base. + cosmossdk.io/indexer/base v0.0.0-00010101000000-000000000000 ) -go 1.22 - -replace cosmossdk.io/indexer/base => ../base +replace cosmossdk.io/indexer/base => ../base \ No newline at end of file diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go deleted file mode 100644 index ce11b98275da..000000000000 --- a/indexer/postgres/indexer.go +++ /dev/null @@ -1,72 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "os" - - "github.com/jackc/pgx/v5" - - indexerbase "cosmossdk.io/indexer/base" -) - -type indexer struct { - ctx context.Context - conn *pgx.Conn -} - -type Options struct { - ConnectionURL string -} - -func NewIndexer(ctx context.Context, opts Options) (indexerbase.Listener, error) { - // get DATABASE_URL from environment - dbUrl := opts.ConnectionURL - if dbUrl == "" { - var ok bool - dbUrl, ok = os.LookupEnv("DATABASE_URL") - if !ok { - return indexerbase.Listener{}, fmt.Errorf("connection URL not set") - } - } - - conn, err := pgx.Connect(ctx, dbUrl) - if err != nil { - return indexerbase.Listener{}, err - } - - i := &indexer{ - ctx: ctx, - conn: conn, - } - - return i.logicalListener() -} - -func (i *indexer) logicalListener() (indexerbase.Listener, error) { - return indexerbase.Listener{ - Initialize: i.initialize, - InitializeModuleSchema: i.initModuleSchema, - StartBlock: i.startBlock, - Commit: i.commit, - }, nil -} - -func (i *indexer) initialize(indexerbase.InitializationData) (int64, error) { - // we don't care about persisting block data yet so just return 0 - return 0, nil -} - -func (i *indexer) initModuleSchema(moduleName string, schema indexerbase.ModuleSchema) (int64, error) { - //for _, _ := range schema.Tables { - //} - return -1, nil -} - -func (i *indexer) startBlock(u uint64) error { - return nil -} - -func (i *indexer) commit() error { - return nil -} diff --git a/indexer/postgres/meta_schema.sql b/indexer/postgres/meta_schema.sql deleted file mode 100644 index a582d21758dd..000000000000 --- a/indexer/postgres/meta_schema.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS indexer; - -CREATE TABLE IF NOT EXISTS module_meta ( - module_name TEXT PRIMARY KEY, -); \ No newline at end of file From 5403d9e82be9a3d8363fef7a1db2d62142166409 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 17 Jun 2024 11:42:04 -0400 Subject: [PATCH 29/79] testing WIP --- indexer/postgres/create_table.go | 28 +++++-- indexer/postgres/gen.go | 15 ---- indexer/postgres/indexer.go | 91 +++++++++++++++++++++++ indexer/postgres/table_mgr.go | 25 +++++++ indexer/postgres/testing/go.mod | 37 +++++++++ indexer/postgres/testing/go.sum | 52 +++++++++++++ indexer/postgres/testing/postgres_test.go | 40 ++++++++++ indexer/testing/fixture.go | 84 +++++++++------------ indexer/testing/go.mod | 7 +- indexer/testing/test_listener.go | 1 - 10 files changed, 308 insertions(+), 72 deletions(-) delete mode 100644 indexer/postgres/gen.go create mode 100644 indexer/postgres/indexer.go create mode 100644 indexer/postgres/table_mgr.go create mode 100644 indexer/postgres/testing/go.mod create mode 100644 indexer/postgres/testing/go.sum create mode 100644 indexer/postgres/testing/postgres_test.go diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index 7c31d83d19a9..cddb29bf0630 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -1,6 +1,8 @@ package postgres import ( + "context" + "database/sql" "fmt" "io" "strings" @@ -8,9 +10,20 @@ import ( indexerbase "cosmossdk.io/indexer/base" ) -// CreateTable generates a CREATE TABLE statement for the object type. -func (s *SQLGenerator) CreateTable(writer io.Writer) error { - _, err := fmt.Fprintf(writer, "CREATE TABLE IF NOT EXISTS %s (\n\t", s.typ.Name) +func (s *TableManager) CreateTable(ctx context.Context, tx *sql.Tx) error { + buf := new(strings.Builder) + err := s.GenCreateTable(buf) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, buf.String()) + return err +} + +// GenCreateTable generates a CREATE TABLE statement for the object type. +func (s *TableManager) GenCreateTable(writer io.Writer) error { + _, err := fmt.Fprintf(writer, "CREATE TABLE IF NOT EXISTS %s (", s.TableName()) if err != nil { return err } @@ -48,7 +61,10 @@ func (s *SQLGenerator) CreateTable(writer io.Writer) error { return err } - _, err = fmt.Fprintf(writer, ");") + _, err = fmt.Fprintf(writer, `); + +GRANT SELECT ON TABLE %s TO PUBLIC; +`, s.TableName()) if err != nil { return err } @@ -56,7 +72,7 @@ func (s *SQLGenerator) CreateTable(writer io.Writer) error { return nil } -func (s *SQLGenerator) createColumnDef(writer io.Writer, field indexerbase.Field) error { +func (s *TableManager) createColumnDef(writer io.Writer, field indexerbase.Field) error { typeStr, err := columnType(field) if err != nil { return err @@ -74,6 +90,6 @@ func (s *SQLGenerator) createColumnDef(writer io.Writer, field indexerbase.Field } } - _, err = fmt.Fprint(writer, " NULL,\\n\\t") + _, err = fmt.Fprint(writer, " NULL,\n\t") return err } diff --git a/indexer/postgres/gen.go b/indexer/postgres/gen.go deleted file mode 100644 index 664d0051ad86..000000000000 --- a/indexer/postgres/gen.go +++ /dev/null @@ -1,15 +0,0 @@ -package postgres - -import ( - indexerbase "cosmossdk.io/indexer/base" -) - -// SQLGenerator is a helper struct that generates SQL for a given object type. -type SQLGenerator struct { - typ indexerbase.ObjectType -} - -// NewSQLGenerator creates a new SQLGenerator for the given object type. -func NewSQLGenerator(typ indexerbase.ObjectType) *SQLGenerator { - return &SQLGenerator{typ: typ} -} diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go new file mode 100644 index 000000000000..855ceac10697 --- /dev/null +++ b/indexer/postgres/indexer.go @@ -0,0 +1,91 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + + indexerbase "cosmossdk.io/indexer/base" +) + +type Indexer struct { + ctx context.Context + db *sql.DB + tx *sql.Tx + modules map[string]*moduleManager +} + +type moduleManager struct { + moduleName string + schema indexerbase.ModuleSchema + tables map[string]*TableManager +} + +type Options struct { + Driver string + ConnectionURL string +} + +func NewIndexer(ctx context.Context, opts Options) (*Indexer, error) { + if opts.Driver == "" { + opts.Driver = "pgx" + } + + if opts.ConnectionURL == "" { + return nil, fmt.Errorf("connection URL not set") + } + + db, err := sql.Open(opts.Driver, opts.ConnectionURL) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + go func() { + <-ctx.Done() + err := db.Close() + if err != nil { + panic(fmt.Sprintf("failed to close database: %v", err)) + } + }() + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to start transaction: %w", err) + } + + return &Indexer{ + ctx: ctx, + db: db, + tx: tx, + modules: map[string]*moduleManager{}, + }, nil +} + +func (i *Indexer) Listener() indexerbase.Listener { + return indexerbase.Listener{ + InitializeModuleSchema: i.initModuleSchema, + } +} + +func (i *Indexer) initModuleSchema(moduleName string, schema indexerbase.ModuleSchema) error { + _, ok := i.modules[moduleName] + if ok { + return fmt.Errorf("module %s already initialized", moduleName) + } + + mm := &moduleManager{ + moduleName: moduleName, + schema: schema, + tables: map[string]*TableManager{}, + } + + for _, typ := range schema.ObjectTypes { + tm := NewTableManager(moduleName, typ) + mm.tables[typ.Name] = tm + err := tm.CreateTable(i.ctx, i.tx) + if err != nil { + return fmt.Errorf("failed to create table for %s in module %s: %w", typ.Name, moduleName, err) + } + } + + return nil +} diff --git a/indexer/postgres/table_mgr.go b/indexer/postgres/table_mgr.go new file mode 100644 index 000000000000..fbe09dced5ab --- /dev/null +++ b/indexer/postgres/table_mgr.go @@ -0,0 +1,25 @@ +package postgres + +import ( + "fmt" + + indexerbase "cosmossdk.io/indexer/base" +) + +// TableManager is a helper struct that generates SQL for a given object type. +type TableManager struct { + moduleName string + typ indexerbase.ObjectType +} + +// NewTableManager creates a new TableManager for the given object type. +func NewTableManager(moduleName string, typ indexerbase.ObjectType) *TableManager { + return &TableManager{ + moduleName: moduleName, + typ: typ, + } +} + +func (t *TableManager) TableName() string { + return fmt.Sprintf("%s_%s", t.moduleName, t.typ.Name) +} diff --git a/indexer/postgres/testing/go.mod b/indexer/postgres/testing/go.mod new file mode 100644 index 000000000000..218dda61adc8 --- /dev/null +++ b/indexer/postgres/testing/go.mod @@ -0,0 +1,37 @@ +module cosmossdk.io/indexer/postgres/testing + +require ( + cosmossdk.io/indexer/postgres v0.0.0 + cosmossdk.io/indexer/testing v0.0.0-00010101000000-000000000000 + github.com/fergusstrange/embedded-postgres v1.27.0 + github.com/hashicorp/consul/sdk v0.16.1 + github.com/jackc/pgx/v5 v5.6.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + cosmossdk.io/indexer/base v0.0.0 // indirect + github.com/brianvoe/gofakeit/v7 v7.0.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.4 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace cosmossdk.io/indexer/base => ../../base + +replace cosmossdk.io/indexer/testing => ../../testing + +replace cosmossdk.io/indexer/postgres => .. + +go 1.22 diff --git a/indexer/postgres/testing/go.sum b/indexer/postgres/testing/go.sum new file mode 100644 index 000000000000..8f3bc81afa81 --- /dev/null +++ b/indexer/postgres/testing/go.sum @@ -0,0 +1,52 @@ +github.com/brianvoe/gofakeit/v7 v7.0.3 h1:tGCt+eYfhTMWE1ko5G2EO1f/yE44yNpIwUb4h32O0wo= +github.com/brianvoe/gofakeit/v7 v7.0.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fergusstrange/embedded-postgres v1.27.0 h1:RAlpWL194IhEpPgeJceTM0ifMJKhiSVxBVIDYB1Jee8= +github.com/fergusstrange/embedded-postgres v1.27.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= +github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= +github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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= diff --git a/indexer/postgres/testing/postgres_test.go b/indexer/postgres/testing/postgres_test.go new file mode 100644 index 000000000000..ae14efca9604 --- /dev/null +++ b/indexer/postgres/testing/postgres_test.go @@ -0,0 +1,40 @@ +package testing + +import ( + "context" + "testing" + + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + "github.com/hashicorp/consul/sdk/freeport" + "github.com/stretchr/testify/require" + + indexertesting "cosmossdk.io/indexer/testing" + + "cosmossdk.io/indexer/postgres" + + _ "github.com/jackc/pgx/v5/stdlib" +) + +func TestPostgresIndexer(t *testing.T) { + dbPort := freeport.GetOne(t) + pgConfig := embeddedpostgres.DefaultConfig().Port(uint32(dbPort)) + dbUrl := pgConfig.GetConnectionURL() + pg := embeddedpostgres.NewDatabase(pgConfig) + require.NoError(t, pg.Start()) + + ctx, cancel := context.WithCancel(context.Background()) + + t.Cleanup(func() { + cancel() + require.NoError(t, pg.Stop()) + }) + + indexer, err := postgres.NewIndexer(ctx, postgres.Options{ + Driver: "pgx", + ConnectionURL: dbUrl, + }) + require.NoError(t, err) + + fixture := indexertesting.NewListenerTestFixture(indexer.Listener(), indexertesting.ListenerTestFixtureOptions{}) + require.NoError(t, fixture.Initialize()) +} diff --git a/indexer/testing/fixture.go b/indexer/testing/fixture.go index 0550ac9b2646..963e43d282d1 100644 --- a/indexer/testing/fixture.go +++ b/indexer/testing/fixture.go @@ -19,7 +19,7 @@ type ListenerTestFixture struct { rndSource rand.Source block uint64 listener indexerbase.Listener - allKeyModule testModule + allKeyModule *testModule } type ListenerTestFixtureOptions struct { @@ -27,10 +27,11 @@ type ListenerTestFixtureOptions struct { } func NewListenerTestFixture(listener indexerbase.Listener, options ListenerTestFixtureOptions) *ListenerTestFixture { + src := rand.NewPCG(1, 2) return &ListenerTestFixture{ - rndSource: rand.NewPCG(1, 2), + rndSource: src, listener: listener, - allKeyModule: mkAllKeysModule(), + allKeyModule: mkAllKeysModule(src), } } @@ -45,26 +46,27 @@ func (f *ListenerTestFixture) Initialize() error { } func (f *ListenerTestFixture) NextBlock() error { - f.block++ - - if f.listener.StartBlock != nil { - err := f.listener.StartBlock(f.block) - if err != nil { - return err - } - } - - err := f.allKeyModule.updater(f.rndSource, &f.listener) - if err != nil { - return err - } - - if f.listener.Commit != nil { - err := f.listener.Commit() - if err != nil { - return err - } - } + // TODO: + //f.block++ + // + //if f.listener.StartBlock != nil { + // err := f.listener.StartBlock(f.block) + // if err != nil { + // return err + // } + //} + // + //err := f.allKeyModule.updater(f.rndSource, &f.listener) + //if err != nil { + // return err + //} + // + //if f.listener.Commit != nil { + // err := f.listener.Commit() + // if err != nil { + // return err + // } + //} return nil } @@ -137,10 +139,11 @@ type testModule struct { type testObjectStore struct { updater func(rand.Source, *indexerbase.Listener) error - state map[any]any + state map[string]kvPair } -type value struct { +type kvPair struct { + key any value any state valueState } @@ -154,18 +157,23 @@ const ( ) func mkAllKeysModule(src rand.Source) *testModule { - mod := &testModule{} + mod := &testModule{ + name: "all_keys", + state: map[string]*testObjectStore{}, + } for i := 1; i < int(maxKind); i++ { kind := indexerbase.Kind(i) typ := mkTestObjectType(kind) mod.schema.ObjectTypes = append(mod.schema.ObjectTypes, typ) - state := map[any]any{} + state := map[string]kvPair{} // generate 5 keys for j := 0; j < 5; j++ { key1 := mkTestValue(src, kind, false) key2 := mkTestValue(src, kind, true) key := []any{key1, key2} - state[key] = nil // initialize as nil + state[fmt.Sprintf("%v", key)] = kvPair{ + key: key, + } } objStore := &testObjectStore{ @@ -174,25 +182,7 @@ func mkAllKeysModule(src rand.Source) *testModule { mod.state[typ.Name] = objStore } - const name = "all_kinds" - return testModule{ - name: name, - schema: schema, - //updater: func(source rand.Source, listener *indexerbase.Listener) error { - // if listener.OnObjectUpdate != nil { - // for i := 1; i < int(maxKind); i++ { - // // two updates per kind - // for j := 0; j < 2; j++ { - // err := listener.OnObjectUpdate(name, mkTestUpdate(source, indexerbase.Kind(i))) - // if err != nil { - // return err - // } - // } - // } - // } - // return nil - //}, - } + return mod } func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectType { diff --git a/indexer/testing/go.mod b/indexer/testing/go.mod index 231dac91f964..884bd5f95f51 100644 --- a/indexer/testing/go.mod +++ b/indexer/testing/go.mod @@ -1,8 +1,9 @@ module cosmossdk.io/indexer/testing -require cosmossdk.io/indexer/base v0.0.0 - -require github.com/brianvoe/gofakeit/v7 v7.0.3 // indirect +require ( + cosmossdk.io/indexer/base v0.0.0 + github.com/brianvoe/gofakeit/v7 v7.0.3 +) replace cosmossdk.io/indexer/base => ../base diff --git a/indexer/testing/test_listener.go b/indexer/testing/test_listener.go index ea0998098529..26bccc118f58 100644 --- a/indexer/testing/test_listener.go +++ b/indexer/testing/test_listener.go @@ -41,6 +41,5 @@ func WriterListener(w io.Writer) indexerbase.Listener { _, err := fmt.Fprintf(w, "OnObjectUpdate: %s: %v\n", moduleName, data) return err }, - CommitCatchupSync: nil, } } From ba706a5e2f8d6575e2aef715358b3dc866dd71a5 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 17 Jun 2024 11:47:58 -0400 Subject: [PATCH 30/79] WIP --- indexer/postgres/col_type.go | 2 ++ indexer/testing/fixture.go | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/indexer/postgres/col_type.go b/indexer/postgres/col_type.go index 0365226e6c69..2c9e1a8278a4 100644 --- a/indexer/postgres/col_type.go +++ b/indexer/postgres/col_type.go @@ -30,6 +30,8 @@ func columnType(field indexerbase.Field) (string, error) { return "BIGINT", nil case indexerbase.Uint64Kind: return "NUMERIC", nil + case indexerbase.IntegerKind: + return "NUMERIC", nil case indexerbase.DecimalKind: return "NUMERIC", nil case indexerbase.Float32Kind: diff --git a/indexer/testing/fixture.go b/indexer/testing/fixture.go index 963e43d282d1..1cda5994708b 100644 --- a/indexer/testing/fixture.go +++ b/indexer/testing/fixture.go @@ -187,7 +187,6 @@ func mkAllKeysModule(src rand.Source) *testModule { func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectType { field := indexerbase.Field{ - Name: fmt.Sprintf("test_%v", kind), Kind: kind, } @@ -211,7 +210,7 @@ func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectType { val2Field.Nullable = true return indexerbase.ObjectType{ - Name: fmt.Sprintf("test_%v", kind), + Name: fmt.Sprintf("test_%#v", kind), KeyFields: []indexerbase.Field{key1Field, key2Field}, ValueFields: []indexerbase.Field{val1Field, val2Field}, } From 50a8c37f7aa52650c8f0d408c8b390b937edd82d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 17 Jun 2024 11:51:29 -0400 Subject: [PATCH 31/79] restore validation code --- indexer/base/field.go | 123 ++++++++++++++++ indexer/base/kind.go | 227 +++++++++++++++++++++++++++++ indexer/base/kind_test.go | 290 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 640 insertions(+) create mode 100644 indexer/base/kind_test.go diff --git a/indexer/base/field.go b/indexer/base/field.go index 713abdeb4c86..fe2fad7105ab 100644 --- a/indexer/base/field.go +++ b/indexer/base/field.go @@ -1,5 +1,7 @@ package indexerbase +import "fmt" + // Field represents a field in an object type. type Field struct { // Name is the name of the field. @@ -17,3 +19,124 @@ type Field struct { // EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind. EnumDefinition EnumDefinition } + +// Validate validates the field. +func (c Field) Validate() error { + // non-empty name + if c.Name == "" { + return fmt.Errorf("field name cannot be empty") + } + + // valid kind + if err := c.Kind.Validate(); err != nil { + return fmt.Errorf("invalid field type for %q: %w", c.Name, err) + } + + // address prefix only valid with Bech32AddressKind + if c.Kind == Bech32AddressKind && c.AddressPrefix == "" { + return fmt.Errorf("missing address prefix for field %q", c.Name) + } else if c.Kind != Bech32AddressKind && c.AddressPrefix != "" { + return fmt.Errorf("address prefix is only valid for field %q with type Bech32AddressKind", c.Name) + } + + // enum definition only valid with EnumKind + if c.Kind == EnumKind { + if err := c.EnumDefinition.Validate(); err != nil { + return fmt.Errorf("invalid enum definition for field %q: %w", c.Name, err) + } + } else if c.Kind != EnumKind && c.EnumDefinition.Name != "" && c.EnumDefinition.Values != nil { + return fmt.Errorf("enum definition is only valid for field %q with type EnumKind", c.Name) + } + + return nil +} + +// Validate validates the enum definition. +func (e EnumDefinition) Validate() error { + if e.Name == "" { + return fmt.Errorf("enum definition name cannot be empty") + } + if len(e.Values) == 0 { + return fmt.Errorf("enum definition values cannot be empty") + } + seen := make(map[string]bool, len(e.Values)) + for i, v := range e.Values { + if v == "" { + return fmt.Errorf("enum definition value at index %d cannot be empty for enum %s", i, e.Name) + } + if seen[v] { + return fmt.Errorf("duplicate enum definition value %q for enum %s", v, e.Name) + } + seen[v] = true + } + return nil +} + +// ValidateValue validates that the value conforms to the field's kind and nullability. +// It currently does not do any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind +// values are valid for their respective types behind conforming to the correct go type. +func (c Field) ValidateValue(value any) error { + if value == nil { + if !c.Nullable { + return fmt.Errorf("field %q cannot be null", c.Name) + } + return nil + } + return c.Kind.ValidateValueType(value) +} + +// ValidateKey validates that the value conforms to the set of fields as a Key in an EntityUpdate. +// See EntityUpdate.Key for documentation on the requirements of such values. +func ValidateKey(fields []Field, value any) error { + if len(fields) == 0 { + return nil + } + + if len(fields) == 1 { + return fields[0].ValidateValue(value) + } + + values, ok := value.([]any) + if !ok { + return fmt.Errorf("expected slice of values for key fields, got %T", value) + } + + if len(fields) != len(values) { + return fmt.Errorf("expected %d key fields, got %d values", len(fields), len(value.([]any))) + } + for i, field := range fields { + if err := field.ValidateValue(values[i]); err != nil { + return fmt.Errorf("invalid value for key field %q: %w", field.Name, err) + } + } + return nil +} + +// ValidateValue validates that the value conforms to the set of fields as a Value in an EntityUpdate. +// See EntityUpdate.Value for documentation on the requirements of such values. +func ValidateValue(fields []Field, value any) error { + valueUpdates, ok := value.(ValueUpdates) + if ok { + fieldMap := map[string]Field{} + for _, field := range fields { + fieldMap[field.Name] = field + } + var errs []error + valueUpdates.Iterate(func(fieldName string, value any) bool { + field, ok := fieldMap[fieldName] + if !ok { + errs = append(errs, fmt.Errorf("unknown field %q in value updates", fieldName)) + } + if err := field.ValidateValue(value); err != nil { + errs = append(errs, fmt.Errorf("invalid value for field %q: %w", fieldName, err)) + } + return true + }) + if len(errs) > 0 { + return fmt.Errorf("validation errors: %v", errs) + } + return nil + } else { + return ValidateKey(fields, value) + } +} diff --git a/indexer/base/kind.go b/indexer/base/kind.go index 064a6543beaf..af772580210a 100644 --- a/indexer/base/kind.go +++ b/indexer/base/kind.go @@ -1,5 +1,11 @@ package indexerbase +import ( + "encoding/json" + "fmt" + "time" +) + // Kind represents the basic type of a field in an object. // Each kind defines the types of go values which should be accepted // by listeners and generated by decoders when providing entity updates. @@ -80,3 +86,224 @@ const ( // or any type that can be marshaled to JSON using json.Marshal. JSONKind ) + +// Validate returns an error if the kind is invalid. +func (t Kind) Validate() error { + if t <= InvalidKind { + return fmt.Errorf("unknown type: %d", t) + } + if t > JSONKind { + return fmt.Errorf("invalid type: %d", t) + } + return nil +} + +// ValidateValueType returns an error if the value does not the type go type specified by the kind. +// Some fields may accept nil values, however, this method does not have any notion of +// nullability. It only checks that the value is of the correct type. +// It also doesn't perform any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind +// values are valid for their respective types. +func (t Kind) ValidateValueType(value any) error { + switch t { + case StringKind: + _, ok := value.(string) + _, ok2 := value.(fmt.Stringer) + if !ok && !ok2 { + return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + } + case BytesKind: + _, ok := value.([]byte) + if !ok { + return fmt.Errorf("expected []byte, got %T", value) + } + case Int8Kind: + _, ok := value.(int8) + if !ok { + return fmt.Errorf("expected int8, got %T", value) + } + case Uint8Kind: + _, ok := value.(uint8) + if !ok { + return fmt.Errorf("expected uint8, got %T", value) + } + case Int16Kind: + _, ok := value.(int16) + if !ok { + return fmt.Errorf("expected int16, got %T", value) + } + case Uint16Kind: + _, ok := value.(uint16) + if !ok { + return fmt.Errorf("expected uint16, got %T", value) + } + case Int32Kind: + _, ok := value.(int32) + if !ok { + return fmt.Errorf("expected int32, got %T", value) + } + case Uint32Kind: + _, ok := value.(uint32) + if !ok { + return fmt.Errorf("expected uint32, got %T", value) + } + case Int64Kind: + _, ok := value.(int64) + if !ok { + return fmt.Errorf("expected int64, got %T", value) + } + case Uint64Kind: + _, ok := value.(uint64) + if !ok { + return fmt.Errorf("expected uint64, got %T", value) + } + case IntegerKind: + _, ok := value.(string) + _, ok2 := value.(fmt.Stringer) + _, ok3 := value.(int64) + if !ok && !ok2 && !ok3 { + return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + } + case DecimalKind: + _, ok := value.(string) + _, ok2 := value.(fmt.Stringer) + if !ok && !ok2 { + return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + } + case BoolKind: + _, ok := value.(bool) + if !ok { + return fmt.Errorf("expected bool, got %T", value) + } + case TimeKind: + _, ok := value.(time.Time) + if !ok { + return fmt.Errorf("expected time.Time, got %T", value) + } + case DurationKind: + _, ok := value.(time.Duration) + if !ok { + return fmt.Errorf("expected time.Duration, got %T", value) + } + case Float32Kind: + _, ok := value.(float32) + if !ok { + return fmt.Errorf("expected float32, got %T", value) + } + case Float64Kind: + _, ok := value.(float64) + if !ok { + return fmt.Errorf("expected float64, got %T", value) + } + case Bech32AddressKind: + _, ok := value.(string) + _, ok2 := value.([]byte) + _, ok3 := value.(fmt.Stringer) + if !ok && !ok2 && !ok3 { + return fmt.Errorf("expected string or []byte, got %T", value) + } + case EnumKind: + _, ok := value.(string) + _, ok2 := value.(fmt.Stringer) + if !ok && !ok2 { + return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + } + case JSONKind: + return nil + default: + return fmt.Errorf("invalid type: %d", t) + } + return nil +} + +// String returns a string representation of the kind. +func (t Kind) String() string { + switch t { + case StringKind: + return "string" + case BytesKind: + return "bytes" + case Int8Kind: + return "int8" + case Uint8Kind: + return "uint8" + case Int16Kind: + return "int16" + case Uint16Kind: + return "uint16" + case Int32Kind: + return "int32" + case Uint32Kind: + return "uint32" + case Int64Kind: + return "int64" + case Uint64Kind: + return "uint64" + case DecimalKind: + return "decimal" + case IntegerKind: + return "integer" + case BoolKind: + return "bool" + case TimeKind: + return "time" + case DurationKind: + return "duration" + case Float32Kind: + return "float32" + case Float64Kind: + return "float64" + case Bech32AddressKind: + return "bech32address" + case EnumKind: + return "enum" + case JSONKind: + return "json" + default: + return "" + } +} + +// KindForGoValue finds the simplest kind that can represent the given go value. It will not, however, +// return kinds such as IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind which all can be +// represented as strings. Generally all values which do not have a more specific type will +// return JSONKind because the framework cannot decide at this point whether the value +// can or cannot be marshaled to JSON. This method should generally only be used as a fallback +// when the kind of a field is not specified more specifically. +func KindForGoValue(value any) Kind { + switch value.(type) { + case string, fmt.Stringer: + return StringKind + case []byte: + return BytesKind + case int8: + return Int8Kind + case uint8: + return Uint8Kind + case int16: + return Int16Kind + case uint16: + return Uint16Kind + case int32: + return Int32Kind + case uint32: + return Uint32Kind + case int64: + return Int64Kind + case uint64: + return Uint64Kind + case float32: + return Float32Kind + case float64: + return Float64Kind + case bool: + return BoolKind + case time.Time: + return TimeKind + case time.Duration: + return DurationKind + case json.RawMessage: + return JSONKind + default: + return JSONKind + } +} diff --git a/indexer/base/kind_test.go b/indexer/base/kind_test.go new file mode 100644 index 000000000000..18075535b067 --- /dev/null +++ b/indexer/base/kind_test.go @@ -0,0 +1,290 @@ +package indexerbase + +import ( + "strings" + "testing" + "time" +) + +func TestKind_Validate(t *testing.T) { + validKinds := []Kind{ + StringKind, + BytesKind, + Int8Kind, + Uint8Kind, + Int16Kind, + Uint16Kind, + Int32Kind, + Uint32Kind, + Int64Kind, + Uint64Kind, + IntegerKind, + DecimalKind, + BoolKind, + EnumKind, + Bech32AddressKind, + } + + for _, kind := range validKinds { + if err := kind.Validate(); err != nil { + t.Errorf("expected valid kind %s to pass validation, got: %v", kind, err) + } + } + + invalidKinds := []Kind{ + Kind(-1), + InvalidKind, + Kind(100), + } + + for _, kind := range invalidKinds { + if err := kind.Validate(); err == nil { + t.Errorf("expected invalid kind %s to fail validation, got: %v", kind, err) + } + } +} + +func TestKind_ValidateValue(t *testing.T) { + tests := []struct { + kind Kind + value any + valid bool + }{ + { + kind: StringKind, + value: "hello", + valid: true, + }, + { + kind: StringKind, + value: &strings.Builder{}, + valid: true, + }, + { + kind: StringKind, + value: []byte("hello"), + valid: false, + }, + { + kind: BytesKind, + value: []byte("hello"), + valid: true, + }, + { + kind: BytesKind, + value: "hello", + valid: false, + }, + { + kind: Int8Kind, + value: int8(1), + valid: true, + }, + { + kind: Int8Kind, + value: int16(1), + valid: false, + }, + { + kind: Uint8Kind, + value: uint8(1), + valid: true, + }, + { + kind: Uint8Kind, + value: uint16(1), + valid: false, + }, + { + kind: Int16Kind, + value: int16(1), + valid: true, + }, + { + kind: Int16Kind, + value: int32(1), + valid: false, + }, + { + kind: Uint16Kind, + value: uint16(1), + valid: true, + }, + { + kind: Uint16Kind, + value: uint32(1), + valid: false, + }, + { + kind: Int32Kind, + value: int32(1), + valid: true, + }, + { + kind: Int32Kind, + value: int64(1), + valid: false, + }, + { + kind: Uint32Kind, + value: uint32(1), + valid: true, + }, + { + kind: Uint32Kind, + value: uint64(1), + valid: false, + }, + { + kind: Int64Kind, + value: int64(1), + valid: true, + }, + { + kind: Int64Kind, + value: int32(1), + valid: false, + }, + { + kind: Uint64Kind, + value: uint64(1), + valid: true, + }, + { + kind: Uint64Kind, + value: uint32(1), + valid: false, + }, + { + kind: IntegerKind, + value: "1", + valid: true, + }, + //{ + // kind: IntegerKind, + // value: (&strings.Builder{}).WriteString("1"), + // valid: true, + //}, + { + kind: IntegerKind, + value: int32(1), + valid: false, + }, + { + kind: IntegerKind, + value: int64(1), + valid: true, + }, + { + kind: DecimalKind, + value: "1.0", + valid: true, + }, + { + kind: DecimalKind, + value: "1", + valid: true, + }, + { + kind: DecimalKind, + value: "1.1e4", + valid: true, + }, + //{ + // kind: DecimalKind, + // value: (&strings.Builder{}).WriteString("1.0"), + // valid: true, + //}, + { + kind: DecimalKind, + value: int32(1), + valid: false, + }, + { + kind: Bech32AddressKind, + value: "cosmos1hsk6jryyqjfhp5g7c0nh4n6dd45ygctnxglp5h", + valid: true, + }, + //{ + // kind: Bech32AddressKind, + // value: (&strings.Builder{}).WriteString("cosmos1hsk6jryyqjfhp5g7c0nh4n6dd45ygctnxglp5h"), + // valid: true, + //}, + { + kind: Bech32AddressKind, + value: 1, + valid: false, + }, + { + kind: BoolKind, + value: true, + valid: true, + }, + { + kind: BoolKind, + value: false, + valid: true, + }, + { + kind: BoolKind, + value: 1, + valid: false, + }, + { + kind: EnumKind, + value: "hello", + valid: true, + }, + //{ + // kind: EnumKind, + // value: (&strings.Builder{}).WriteString("hello"), + // valid: true, + //}, + { + kind: EnumKind, + value: 1, + valid: false, + }, + { + kind: TimeKind, + value: time.Now(), + valid: true, + }, + { + kind: TimeKind, + value: "hello", + valid: false, + }, + { + kind: DurationKind, + value: time.Second, + valid: true, + }, + { + kind: DurationKind, + value: "hello", + valid: false, + }, + { + kind: Float32Kind, + value: float32(1.0), + valid: true, + }, + { + kind: Float32Kind, + value: float64(1.0), + valid: false, + }, + // TODO float64, json + } + + for i, tt := range tests { + err := tt.kind.ValidateValueType(tt.value) + if tt.valid && err != nil { + t.Errorf("test %d: expected valid value %v for kind %s to pass validation, got: %v", i, tt.value, tt.kind, err) + } + if !tt.valid && err == nil { + t.Errorf("test %d: expected invalid value %v for kind %s to fail validation, got: %v", i, tt.value, tt.kind, err) + } + } +} From f4de20106d8ce81907d9a1d9ce89c83aaf8b7df9 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 17 Jun 2024 12:12:43 -0400 Subject: [PATCH 32/79] working create table --- indexer/base/field.go | 12 ++++++------ indexer/base/kind.go | 4 ++-- indexer/postgres/col_type.go | 6 +++++- indexer/testing/fixture.go | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/indexer/base/field.go b/indexer/base/field.go index fe2fad7105ab..23e3f799b96a 100644 --- a/indexer/base/field.go +++ b/indexer/base/field.go @@ -75,7 +75,7 @@ func (e EnumDefinition) Validate() error { // ValidateValue validates that the value conforms to the field's kind and nullability. // It currently does not do any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind // values are valid for their respective types behind conforming to the correct go type. -func (c Field) ValidateValue(value any) error { +func (c Field) ValidateValue(value interface{}) error { if value == nil { if !c.Nullable { return fmt.Errorf("field %q cannot be null", c.Name) @@ -87,7 +87,7 @@ func (c Field) ValidateValue(value any) error { // ValidateKey validates that the value conforms to the set of fields as a Key in an EntityUpdate. // See EntityUpdate.Key for documentation on the requirements of such values. -func ValidateKey(fields []Field, value any) error { +func ValidateKey(fields []Field, value interface{}) error { if len(fields) == 0 { return nil } @@ -96,13 +96,13 @@ func ValidateKey(fields []Field, value any) error { return fields[0].ValidateValue(value) } - values, ok := value.([]any) + values, ok := value.([]interface{}) if !ok { return fmt.Errorf("expected slice of values for key fields, got %T", value) } if len(fields) != len(values) { - return fmt.Errorf("expected %d key fields, got %d values", len(fields), len(value.([]any))) + return fmt.Errorf("expected %d key fields, got %d values", len(fields), len(value.([]interface{}))) } for i, field := range fields { if err := field.ValidateValue(values[i]); err != nil { @@ -114,7 +114,7 @@ func ValidateKey(fields []Field, value any) error { // ValidateValue validates that the value conforms to the set of fields as a Value in an EntityUpdate. // See EntityUpdate.Value for documentation on the requirements of such values. -func ValidateValue(fields []Field, value any) error { +func ValidateValue(fields []Field, value interface{}) error { valueUpdates, ok := value.(ValueUpdates) if ok { fieldMap := map[string]Field{} @@ -122,7 +122,7 @@ func ValidateValue(fields []Field, value any) error { fieldMap[field.Name] = field } var errs []error - valueUpdates.Iterate(func(fieldName string, value any) bool { + valueUpdates.Iterate(func(fieldName string, value interface{}) bool { field, ok := fieldMap[fieldName] if !ok { errs = append(errs, fmt.Errorf("unknown field %q in value updates", fieldName)) diff --git a/indexer/base/kind.go b/indexer/base/kind.go index af772580210a..be6351bf9442 100644 --- a/indexer/base/kind.go +++ b/indexer/base/kind.go @@ -103,7 +103,7 @@ func (t Kind) Validate() error { // nullability. It only checks that the value is of the correct type. // It also doesn't perform any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind // values are valid for their respective types. -func (t Kind) ValidateValueType(value any) error { +func (t Kind) ValidateValueType(value interface{}) error { switch t { case StringKind: _, ok := value.(string) @@ -269,7 +269,7 @@ func (t Kind) String() string { // return JSONKind because the framework cannot decide at this point whether the value // can or cannot be marshaled to JSON. This method should generally only be used as a fallback // when the kind of a field is not specified more specifically. -func KindForGoValue(value any) Kind { +func KindForGoValue(value interface{}) Kind { switch value.(type) { case string, fmt.Stringer: return StringKind diff --git a/indexer/postgres/col_type.go b/indexer/postgres/col_type.go index 2c9e1a8278a4..fe7d4b0aa260 100644 --- a/indexer/postgres/col_type.go +++ b/indexer/postgres/col_type.go @@ -39,11 +39,15 @@ func columnType(field indexerbase.Field) (string, error) { case indexerbase.Float64Kind: return "DOUBLE PRECISION", nil case indexerbase.EnumKind: - return "TEXT", fmt.Errorf("enums not supported yet") + return "TEXT", nil // TODO: create enum type case indexerbase.JSONKind: return "JSONB", nil case indexerbase.Bech32AddressKind: return "TEXT", nil + case indexerbase.TimeKind: + return "TIMESTAMPTZ", nil // TODO: preserve nanos, one idea store as integer and then have generated TIMESTAMPTZ column + case indexerbase.DurationKind: + return "INTERVAL", nil // TODO: preserve nanos, see above default: return "", fmt.Errorf("unsupported kind %v for field %s", field.Kind, field.Name) } diff --git a/indexer/testing/fixture.go b/indexer/testing/fixture.go index 1cda5994708b..45fc57d5df0f 100644 --- a/indexer/testing/fixture.go +++ b/indexer/testing/fixture.go @@ -210,7 +210,7 @@ func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectType { val2Field.Nullable = true return indexerbase.ObjectType{ - Name: fmt.Sprintf("test_%#v", kind), + Name: fmt.Sprintf("test_%v", kind), KeyFields: []indexerbase.Field{key1Field, key2Field}, ValueFields: []indexerbase.Field{val1Field, val2Field}, } From 92dc11202c8c3717da2096ba77e6b1f595c1bf8a Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 19:19:46 +0200 Subject: [PATCH 33/79] merge wip branch work --- indexer/base/README.md | 5 - indexer/base/decoder.go | 27 - indexer/base/enum.go | 10 - indexer/base/field.go | 142 - indexer/base/go.sum | 0 indexer/base/kind_test.go | 290 -- indexer/base/listener.go | 120 - indexer/base/module_schema.go | 8 - indexer/base/object_type.go | 23 - indexer/postgres/base.sql | 33 + indexer/postgres/col_type.go | 54 - indexer/postgres/column.go | 116 + indexer/postgres/count.go | 14 + indexer/postgres/create_table.go | 93 +- indexer/postgres/delete.go | 58 + indexer/postgres/enum.go | 86 + indexer/postgres/go.mod | 19 +- indexer/postgres/go.sum | 14 +- indexer/postgres/graphql-proxy/.gitignore | 24 + indexer/postgres/graphql-proxy/README.md | 47 + .../postgres/graphql-proxy/astro.config.mjs | 6 + indexer/postgres/graphql-proxy/package.json | 19 + indexer/postgres/graphql-proxy/pnpm-lock.yaml | 4311 +++++++++++++++++ .../postgres/graphql-proxy/public/favicon.svg | 9 + indexer/postgres/graphql-proxy/src/env.d.ts | 1 + .../graphql-proxy/src/pages/graphiql.astro | 81 + .../graphql-proxy/src/pages/graphql.ts | 47 + .../graphql-proxy/src/pages/index.astro | 11 + indexer/postgres/graphql-proxy/tsconfig.json | 3 + indexer/postgres/graphql.go | 97 + indexer/postgres/indexer.go | 161 +- indexer/postgres/insert_update.go | 101 + indexer/postgres/module_mgr.go | 57 + indexer/postgres/params.go | 117 + indexer/postgres/select.go | 91 + indexer/postgres/table_mgr.go | 34 +- indexer/postgres/testing/app/main.go | 100 + indexer/postgres/testing/go.mod | 19 +- indexer/postgres/testing/go.sum | 30 +- indexer/postgres/testing/postgres_test.go | 91 +- indexer/postgres/where.go | 58 + indexer/testing/README.md | 4 - indexer/testing/fixture.go | 315 -- indexer/testing/fixture_test.go | 17 - indexer/testing/go.mod | 10 - indexer/testing/go.sum | 2 - indexer/testing/test_listener.go | 45 - {indexer/base => schema}/CHANGELOG.md | 0 schema/README.md | 13 + schema/appdata/README.md | 32 + schema/appdata/async.go | 115 + schema/appdata/data.go | 99 + schema/appdata/debug.go | 31 + schema/appdata/listener.go | 50 + schema/appdata/mux.go | 121 + schema/appdata/packet.go | 58 + schema/decoder.go | 49 + schema/decoding/README.md | 42 + schema/decoding/middleware.go | 87 + schema/decoding/resolver.go | 71 + schema/decoding/sync.go | 8 + schema/enum.go | 50 + schema/enum_test.go | 106 + schema/field.go | 82 + schema/field_test.go | 183 + schema/fields.go | 71 + schema/fields_test.go | 143 + {indexer/base => schema}/go.mod | 2 +- schema/indexing/indexer.go | 36 + schema/indexing/logger.go | 1 + schema/indexing/manager.go | 70 + {indexer/base => schema}/kind.go | 229 +- schema/kind_test.go | 261 + schema/log/logger.go | 31 + schema/module_schema.go | 74 + schema/module_schema_test.go | 196 + schema/name.go | 15 + schema/name_test.go | 31 + schema/object_type.go | 87 + schema/object_type_test.go | 243 + {indexer/base => schema}/object_update.go | 25 +- schema/object_update_test.go | 52 + {indexer => schema}/testing/CHANGELOG.md | 0 schema/testing/README.md | 4 + schema/testing/app.go | 20 + schema/testing/app_test.go | 18 + schema/testing/appdata/app_data.go | 131 + schema/testing/appdata/app_data_test.go | 30 + schema/testing/appdata/json.go | 40 + .../testdata/app_sim_example_schema.txt | 91 + schema/testing/appdata/write_listener.go | 41 + schema/testing/enum.go | 18 + schema/testing/enum_test.go | 15 + schema/testing/example_schema.go | 198 + schema/testing/field.go | 168 + schema/testing/field_test.go | 37 + schema/testing/go.mod | 20 + schema/testing/go.sum | 18 + schema/testing/module_schema.go | 31 + schema/testing/module_schema_test.go | 15 + schema/testing/name.go | 9 + schema/testing/name_test.go | 17 + schema/testing/object.go | 122 + schema/testing/object_test.go | 31 + schema/testing/statesim/app.go | 103 + schema/testing/statesim/module.go | 72 + schema/testing/statesim/object.go | 117 + schema/testing/statesim/options.go | 5 + schema/unique.go | 5 + 109 files changed, 9862 insertions(+), 1298 deletions(-) delete mode 100644 indexer/base/README.md delete mode 100644 indexer/base/decoder.go delete mode 100644 indexer/base/enum.go delete mode 100644 indexer/base/field.go delete mode 100644 indexer/base/go.sum delete mode 100644 indexer/base/kind_test.go delete mode 100644 indexer/base/listener.go delete mode 100644 indexer/base/module_schema.go delete mode 100644 indexer/base/object_type.go create mode 100644 indexer/postgres/base.sql delete mode 100644 indexer/postgres/col_type.go create mode 100644 indexer/postgres/column.go create mode 100644 indexer/postgres/count.go create mode 100644 indexer/postgres/delete.go create mode 100644 indexer/postgres/enum.go create mode 100644 indexer/postgres/graphql-proxy/.gitignore create mode 100644 indexer/postgres/graphql-proxy/README.md create mode 100644 indexer/postgres/graphql-proxy/astro.config.mjs create mode 100644 indexer/postgres/graphql-proxy/package.json create mode 100644 indexer/postgres/graphql-proxy/pnpm-lock.yaml create mode 100644 indexer/postgres/graphql-proxy/public/favicon.svg create mode 100644 indexer/postgres/graphql-proxy/src/env.d.ts create mode 100644 indexer/postgres/graphql-proxy/src/pages/graphiql.astro create mode 100644 indexer/postgres/graphql-proxy/src/pages/graphql.ts create mode 100644 indexer/postgres/graphql-proxy/src/pages/index.astro create mode 100644 indexer/postgres/graphql-proxy/tsconfig.json create mode 100644 indexer/postgres/graphql.go create mode 100644 indexer/postgres/insert_update.go create mode 100644 indexer/postgres/module_mgr.go create mode 100644 indexer/postgres/params.go create mode 100644 indexer/postgres/select.go create mode 100644 indexer/postgres/testing/app/main.go create mode 100644 indexer/postgres/where.go delete mode 100644 indexer/testing/README.md delete mode 100644 indexer/testing/fixture.go delete mode 100644 indexer/testing/fixture_test.go delete mode 100644 indexer/testing/go.mod delete mode 100644 indexer/testing/go.sum delete mode 100644 indexer/testing/test_listener.go rename {indexer/base => schema}/CHANGELOG.md (100%) create mode 100644 schema/README.md create mode 100644 schema/appdata/README.md create mode 100644 schema/appdata/async.go create mode 100644 schema/appdata/data.go create mode 100644 schema/appdata/debug.go create mode 100644 schema/appdata/listener.go create mode 100644 schema/appdata/mux.go create mode 100644 schema/appdata/packet.go create mode 100644 schema/decoder.go create mode 100644 schema/decoding/README.md create mode 100644 schema/decoding/middleware.go create mode 100644 schema/decoding/resolver.go create mode 100644 schema/decoding/sync.go create mode 100644 schema/enum.go create mode 100644 schema/enum_test.go create mode 100644 schema/field.go create mode 100644 schema/field_test.go create mode 100644 schema/fields.go create mode 100644 schema/fields_test.go rename {indexer/base => schema}/go.mod (88%) create mode 100644 schema/indexing/indexer.go create mode 100644 schema/indexing/logger.go create mode 100644 schema/indexing/manager.go rename {indexer/base => schema}/kind.go (59%) create mode 100644 schema/kind_test.go create mode 100644 schema/log/logger.go create mode 100644 schema/module_schema.go create mode 100644 schema/module_schema_test.go create mode 100644 schema/name.go create mode 100644 schema/name_test.go create mode 100644 schema/object_type.go create mode 100644 schema/object_type_test.go rename {indexer/base => schema}/object_update.go (82%) create mode 100644 schema/object_update_test.go rename {indexer => schema}/testing/CHANGELOG.md (100%) create mode 100644 schema/testing/README.md create mode 100644 schema/testing/app.go create mode 100644 schema/testing/app_test.go create mode 100644 schema/testing/appdata/app_data.go create mode 100644 schema/testing/appdata/app_data_test.go create mode 100644 schema/testing/appdata/json.go create mode 100644 schema/testing/appdata/testdata/app_sim_example_schema.txt create mode 100644 schema/testing/appdata/write_listener.go create mode 100644 schema/testing/enum.go create mode 100644 schema/testing/enum_test.go create mode 100644 schema/testing/example_schema.go create mode 100644 schema/testing/field.go create mode 100644 schema/testing/field_test.go create mode 100644 schema/testing/go.mod create mode 100644 schema/testing/go.sum create mode 100644 schema/testing/module_schema.go create mode 100644 schema/testing/module_schema_test.go create mode 100644 schema/testing/name.go create mode 100644 schema/testing/name_test.go create mode 100644 schema/testing/object.go create mode 100644 schema/testing/object_test.go create mode 100644 schema/testing/statesim/app.go create mode 100644 schema/testing/statesim/module.go create mode 100644 schema/testing/statesim/object.go create mode 100644 schema/testing/statesim/options.go create mode 100644 schema/unique.go diff --git a/indexer/base/README.md b/indexer/base/README.md deleted file mode 100644 index 0b96a27dc63c..000000000000 --- a/indexer/base/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Indexer Base - -The indexer base module is designed to provide a stable, zero-dependency base layer for the built-in indexer functionality. Packages that integrate with the indexer should feel free to depend on this package without fear of any external dependencies being pulled in. - -The basic types for specifying index sources, targets and decoders are provided here along with a basic engine that ties these together. A package wishing to be an indexing source could accept an instance of `Engine` directly to be compatible with indexing. A package wishing to be a decoder can use the `Entity` and `Table` types. A package defining an indexing target should implement the `Indexer` interface. \ No newline at end of file diff --git a/indexer/base/decoder.go b/indexer/base/decoder.go deleted file mode 100644 index fb745c2ac6fe..000000000000 --- a/indexer/base/decoder.go +++ /dev/null @@ -1,27 +0,0 @@ -package indexerbase - -// DecodableModule is an interface that modules can implement to provide a ModuleDecoder. -// Usually these modules would also implement appmodule.AppModule, but that is not included -// to keep this package free of any dependencies. -type DecodableModule interface { - - // ModuleDecoder returns a ModuleDecoder for the module. - ModuleDecoder() (ModuleDecoder, error) -} - -// ModuleDecoder is a struct that contains the schema and a KVDecoder for a module. -type ModuleDecoder struct { - // Schema is the schema for the module. - Schema ModuleSchema - - // KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. - // If modules pass logical updates directly to the engine and don't require logical decoding of raw bytes, - // then this function should be nil. - KVDecoder KVDecoder -} - -// KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. -// If the KV-pair doesn't represent an object update, the function should return false -// as the second return value. Error should only be non-nil when the decoder expected -// to parse a valid update and was unable to. -type KVDecoder = func(key, value []byte) (ObjectUpdate, bool, error) diff --git a/indexer/base/enum.go b/indexer/base/enum.go deleted file mode 100644 index b1275d8a7a95..000000000000 --- a/indexer/base/enum.go +++ /dev/null @@ -1,10 +0,0 @@ -package indexerbase - -// EnumDefinition represents the definition of an enum type. -type EnumDefinition struct { - // Name is the name of the enum type. - Name string - - // Values is a list of distinct values that are part of the enum type. - Values []string -} diff --git a/indexer/base/field.go b/indexer/base/field.go deleted file mode 100644 index 23e3f799b96a..000000000000 --- a/indexer/base/field.go +++ /dev/null @@ -1,142 +0,0 @@ -package indexerbase - -import "fmt" - -// Field represents a field in an object type. -type Field struct { - // Name is the name of the field. - Name string - - // Kind is the basic type of the field. - Kind Kind - - // Nullable indicates whether null values are accepted for the field. - Nullable bool - - // AddressPrefix is the address prefix of the field's kind, currently only used for Bech32AddressKind. - AddressPrefix string - - // EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind. - EnumDefinition EnumDefinition -} - -// Validate validates the field. -func (c Field) Validate() error { - // non-empty name - if c.Name == "" { - return fmt.Errorf("field name cannot be empty") - } - - // valid kind - if err := c.Kind.Validate(); err != nil { - return fmt.Errorf("invalid field type for %q: %w", c.Name, err) - } - - // address prefix only valid with Bech32AddressKind - if c.Kind == Bech32AddressKind && c.AddressPrefix == "" { - return fmt.Errorf("missing address prefix for field %q", c.Name) - } else if c.Kind != Bech32AddressKind && c.AddressPrefix != "" { - return fmt.Errorf("address prefix is only valid for field %q with type Bech32AddressKind", c.Name) - } - - // enum definition only valid with EnumKind - if c.Kind == EnumKind { - if err := c.EnumDefinition.Validate(); err != nil { - return fmt.Errorf("invalid enum definition for field %q: %w", c.Name, err) - } - } else if c.Kind != EnumKind && c.EnumDefinition.Name != "" && c.EnumDefinition.Values != nil { - return fmt.Errorf("enum definition is only valid for field %q with type EnumKind", c.Name) - } - - return nil -} - -// Validate validates the enum definition. -func (e EnumDefinition) Validate() error { - if e.Name == "" { - return fmt.Errorf("enum definition name cannot be empty") - } - if len(e.Values) == 0 { - return fmt.Errorf("enum definition values cannot be empty") - } - seen := make(map[string]bool, len(e.Values)) - for i, v := range e.Values { - if v == "" { - return fmt.Errorf("enum definition value at index %d cannot be empty for enum %s", i, e.Name) - } - if seen[v] { - return fmt.Errorf("duplicate enum definition value %q for enum %s", v, e.Name) - } - seen[v] = true - } - return nil -} - -// ValidateValue validates that the value conforms to the field's kind and nullability. -// It currently does not do any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind -// values are valid for their respective types behind conforming to the correct go type. -func (c Field) ValidateValue(value interface{}) error { - if value == nil { - if !c.Nullable { - return fmt.Errorf("field %q cannot be null", c.Name) - } - return nil - } - return c.Kind.ValidateValueType(value) -} - -// ValidateKey validates that the value conforms to the set of fields as a Key in an EntityUpdate. -// See EntityUpdate.Key for documentation on the requirements of such values. -func ValidateKey(fields []Field, value interface{}) error { - if len(fields) == 0 { - return nil - } - - if len(fields) == 1 { - return fields[0].ValidateValue(value) - } - - values, ok := value.([]interface{}) - if !ok { - return fmt.Errorf("expected slice of values for key fields, got %T", value) - } - - if len(fields) != len(values) { - return fmt.Errorf("expected %d key fields, got %d values", len(fields), len(value.([]interface{}))) - } - for i, field := range fields { - if err := field.ValidateValue(values[i]); err != nil { - return fmt.Errorf("invalid value for key field %q: %w", field.Name, err) - } - } - return nil -} - -// ValidateValue validates that the value conforms to the set of fields as a Value in an EntityUpdate. -// See EntityUpdate.Value for documentation on the requirements of such values. -func ValidateValue(fields []Field, value interface{}) error { - valueUpdates, ok := value.(ValueUpdates) - if ok { - fieldMap := map[string]Field{} - for _, field := range fields { - fieldMap[field.Name] = field - } - var errs []error - valueUpdates.Iterate(func(fieldName string, value interface{}) bool { - field, ok := fieldMap[fieldName] - if !ok { - errs = append(errs, fmt.Errorf("unknown field %q in value updates", fieldName)) - } - if err := field.ValidateValue(value); err != nil { - errs = append(errs, fmt.Errorf("invalid value for field %q: %w", fieldName, err)) - } - return true - }) - if len(errs) > 0 { - return fmt.Errorf("validation errors: %v", errs) - } - return nil - } else { - return ValidateKey(fields, value) - } -} diff --git a/indexer/base/go.sum b/indexer/base/go.sum deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/indexer/base/kind_test.go b/indexer/base/kind_test.go deleted file mode 100644 index 18075535b067..000000000000 --- a/indexer/base/kind_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package indexerbase - -import ( - "strings" - "testing" - "time" -) - -func TestKind_Validate(t *testing.T) { - validKinds := []Kind{ - StringKind, - BytesKind, - Int8Kind, - Uint8Kind, - Int16Kind, - Uint16Kind, - Int32Kind, - Uint32Kind, - Int64Kind, - Uint64Kind, - IntegerKind, - DecimalKind, - BoolKind, - EnumKind, - Bech32AddressKind, - } - - for _, kind := range validKinds { - if err := kind.Validate(); err != nil { - t.Errorf("expected valid kind %s to pass validation, got: %v", kind, err) - } - } - - invalidKinds := []Kind{ - Kind(-1), - InvalidKind, - Kind(100), - } - - for _, kind := range invalidKinds { - if err := kind.Validate(); err == nil { - t.Errorf("expected invalid kind %s to fail validation, got: %v", kind, err) - } - } -} - -func TestKind_ValidateValue(t *testing.T) { - tests := []struct { - kind Kind - value any - valid bool - }{ - { - kind: StringKind, - value: "hello", - valid: true, - }, - { - kind: StringKind, - value: &strings.Builder{}, - valid: true, - }, - { - kind: StringKind, - value: []byte("hello"), - valid: false, - }, - { - kind: BytesKind, - value: []byte("hello"), - valid: true, - }, - { - kind: BytesKind, - value: "hello", - valid: false, - }, - { - kind: Int8Kind, - value: int8(1), - valid: true, - }, - { - kind: Int8Kind, - value: int16(1), - valid: false, - }, - { - kind: Uint8Kind, - value: uint8(1), - valid: true, - }, - { - kind: Uint8Kind, - value: uint16(1), - valid: false, - }, - { - kind: Int16Kind, - value: int16(1), - valid: true, - }, - { - kind: Int16Kind, - value: int32(1), - valid: false, - }, - { - kind: Uint16Kind, - value: uint16(1), - valid: true, - }, - { - kind: Uint16Kind, - value: uint32(1), - valid: false, - }, - { - kind: Int32Kind, - value: int32(1), - valid: true, - }, - { - kind: Int32Kind, - value: int64(1), - valid: false, - }, - { - kind: Uint32Kind, - value: uint32(1), - valid: true, - }, - { - kind: Uint32Kind, - value: uint64(1), - valid: false, - }, - { - kind: Int64Kind, - value: int64(1), - valid: true, - }, - { - kind: Int64Kind, - value: int32(1), - valid: false, - }, - { - kind: Uint64Kind, - value: uint64(1), - valid: true, - }, - { - kind: Uint64Kind, - value: uint32(1), - valid: false, - }, - { - kind: IntegerKind, - value: "1", - valid: true, - }, - //{ - // kind: IntegerKind, - // value: (&strings.Builder{}).WriteString("1"), - // valid: true, - //}, - { - kind: IntegerKind, - value: int32(1), - valid: false, - }, - { - kind: IntegerKind, - value: int64(1), - valid: true, - }, - { - kind: DecimalKind, - value: "1.0", - valid: true, - }, - { - kind: DecimalKind, - value: "1", - valid: true, - }, - { - kind: DecimalKind, - value: "1.1e4", - valid: true, - }, - //{ - // kind: DecimalKind, - // value: (&strings.Builder{}).WriteString("1.0"), - // valid: true, - //}, - { - kind: DecimalKind, - value: int32(1), - valid: false, - }, - { - kind: Bech32AddressKind, - value: "cosmos1hsk6jryyqjfhp5g7c0nh4n6dd45ygctnxglp5h", - valid: true, - }, - //{ - // kind: Bech32AddressKind, - // value: (&strings.Builder{}).WriteString("cosmos1hsk6jryyqjfhp5g7c0nh4n6dd45ygctnxglp5h"), - // valid: true, - //}, - { - kind: Bech32AddressKind, - value: 1, - valid: false, - }, - { - kind: BoolKind, - value: true, - valid: true, - }, - { - kind: BoolKind, - value: false, - valid: true, - }, - { - kind: BoolKind, - value: 1, - valid: false, - }, - { - kind: EnumKind, - value: "hello", - valid: true, - }, - //{ - // kind: EnumKind, - // value: (&strings.Builder{}).WriteString("hello"), - // valid: true, - //}, - { - kind: EnumKind, - value: 1, - valid: false, - }, - { - kind: TimeKind, - value: time.Now(), - valid: true, - }, - { - kind: TimeKind, - value: "hello", - valid: false, - }, - { - kind: DurationKind, - value: time.Second, - valid: true, - }, - { - kind: DurationKind, - value: "hello", - valid: false, - }, - { - kind: Float32Kind, - value: float32(1.0), - valid: true, - }, - { - kind: Float32Kind, - value: float64(1.0), - valid: false, - }, - // TODO float64, json - } - - for i, tt := range tests { - err := tt.kind.ValidateValueType(tt.value) - if tt.valid && err != nil { - t.Errorf("test %d: expected valid value %v for kind %s to pass validation, got: %v", i, tt.value, tt.kind, err) - } - if !tt.valid && err == nil { - t.Errorf("test %d: expected invalid value %v for kind %s to fail validation, got: %v", i, tt.value, tt.kind, err) - } - } -} diff --git a/indexer/base/listener.go b/indexer/base/listener.go deleted file mode 100644 index 488ed87d6bbc..000000000000 --- a/indexer/base/listener.go +++ /dev/null @@ -1,120 +0,0 @@ -package indexerbase - -import ( - "encoding/json" -) - -// Listener is an interface that defines methods for listening to both raw and logical blockchain data. -// It is valid for any of the methods to be nil, in which case the listener will not be called for that event. -// Listeners should understand the guarantees that are provided by the source they are listening to and -// understand which methods will or will not be called. For instance, most blockchains will not do logical -// decoding of data out of the box, so the InitializeModuleSchema and OnObjectUpdate methods will not be called. -// These methods will only be called when listening logical decoding is setup. -type Listener struct { - // Initialize is called when the listener is initialized before any other methods are called. - // The lastBlockPersisted return value should be the last block height the listener persisted if it is - // persisting block data, 0 if it is not interested in persisting block data, or -1 if it is - // persisting block data but has not persisted any data yet. This check allows the indexer - // framework to ensure that the listener has not missed blocks. - Initialize func(InitializationData) (lastBlockPersisted int64, err error) - - // StartBlock is called at the beginning of processing a block. - StartBlock func(uint64) error - - // OnBlockHeader is called when a block header is received. - OnBlockHeader func(BlockHeaderData) error - - // OnTx is called when a transaction is received. - OnTx func(TxData) error - - // OnEvent is called when an event is received. - OnEvent func(EventData) error - - // OnKVPair is called when a key-value has been written to the store for a given module. - OnKVPair func(moduleName string, key, value []byte, delete bool) error - - // Commit is called when state is commited, usually at the end of a block. Any - // indexers should commit their data when this is called and return an error if - // they are unable to commit. - Commit func() error - - // InitializeModuleSchema should be called whenever the blockchain process starts OR whenever - // logical decoding of a module is initiated. An indexer listening to this event - // should ensure that they have performed whatever initialization steps (such as database - // migrations) required to receive OnObjectUpdate events for the given module. If the - // indexer's schema is incompatible with the module's on-chain schema, the listener should return - // an error. - InitializeModuleSchema func(module string, schema ModuleSchema) error - - // OnObjectUpdate is called whenever an object is updated in a module's state. This is only called - // when logical data is available. It should be assumed that the same data in raw form - // is also passed to OnKVPair. - OnObjectUpdate func(module string, update ObjectUpdate) error -} - -// InitializationData represents initialization data that is passed to a listener. -type InitializationData struct { - - // HasEventAlignedWrites indicates that the blockchain data source will emit KV-pair events - // in an order aligned with transaction, message and event callbacks. If this is true - // then indexers can assume that KV-pair data is associated with these specific transactions, messages - // and events. This may be useful for indexers which store a log of all operations (such as immutable - // or version controlled databases) so that the history log can include fine grain correlation between - // state updates and transactions, messages and events. If this value is false, then indexers should - // assume that KV-pair data occurs out of order with respect to transaction, message and event callbacks - - // the only safe assumption being that KV-pair data is associated with the block in which it was emitted. - HasEventAlignedWrites bool -} - -// BlockHeaderData represents the raw block header data that is passed to a listener. -type BlockHeaderData struct { - // Height is the height of the block. - Height uint64 - - // Bytes is the raw byte representation of the block header. - Bytes ToBytes - - // JSON is the JSON representation of the block header. It should generally be a JSON object. - JSON ToJSON -} - -// TxData represents the raw transaction data that is passed to a listener. -type TxData struct { - // TxIndex is the index of the transaction in the block. - TxIndex int32 - - // Bytes is the raw byte representation of the transaction. - Bytes ToBytes - - // JSON is the JSON representation of the transaction. It should generally be a JSON object. - JSON ToJSON -} - -// EventData represents event data that is passed to a listener. -type EventData struct { - // TxIndex is the index of the transaction in the block to which this event is associated. - // It should be set to a negative number if the event is not associated with a transaction. - // Canonically -1 should be used to represent begin block processing and -2 should be used to - // represent end block processing. - TxIndex int32 - - // MsgIndex is the index of the message in the transaction to which this event is associated. - // If TxIndex is negative, this index could correspond to the index of the message in - // begin or end block processing if such indexes exist, or it can be set to zero. - MsgIndex uint32 - - // EventIndex is the index of the event in the message to which this event is associated. - EventIndex uint32 - - // Type is the type of the event. - Type string - - // Data is the JSON representation of the event data. It should generally be a JSON object. - Data ToJSON -} - -// ToBytes is a function that lazily returns the raw byte representation of data. -type ToBytes = func() ([]byte, error) - -// ToJSON is a function that lazily returns the JSON representation of data. -type ToJSON = func() (json.RawMessage, error) diff --git a/indexer/base/module_schema.go b/indexer/base/module_schema.go deleted file mode 100644 index 45da535e87e3..000000000000 --- a/indexer/base/module_schema.go +++ /dev/null @@ -1,8 +0,0 @@ -package indexerbase - -// ModuleSchema represents the logical schema of a module for purposes of indexing and querying. -type ModuleSchema struct { - - // ObjectTypes describe the types of objects that are part of the module's schema. - ObjectTypes []ObjectType -} diff --git a/indexer/base/object_type.go b/indexer/base/object_type.go deleted file mode 100644 index 06f8adbc92a7..000000000000 --- a/indexer/base/object_type.go +++ /dev/null @@ -1,23 +0,0 @@ -package indexerbase - -// ObjectType describes an object type a module schema. -type ObjectType struct { - // Name is the name of the object. - Name string - - // KeyFields is a list of fields that make up the primary key of the object. - // It can be empty in which case indexers should assume that this object is - // a singleton and ony has one value. - KeyFields []Field - - // ValueFields is a list of fields that are not part of the primary key of the object. - // It can be empty in the case where all fields are part of the primary key. - ValueFields []Field - - // RetainDeletions is a flag that indicates whether the indexer should retain - // deleted rows in the database and flag them as deleted rather than actually - // deleting the row. For many types of data in state, the data is deleted even - // though it is still valid in order to save space. Indexers will want to have - // the option of retaining such data and distinguishing from other "true" deletions. - RetainDeletions bool -} diff --git a/indexer/postgres/base.sql b/indexer/postgres/base.sql new file mode 100644 index 000000000000..b73d75140183 --- /dev/null +++ b/indexer/postgres/base.sql @@ -0,0 +1,33 @@ +CREATE SCHEMA IF NOT EXISTS indexer; + +CREATE TABLE IF NOT EXISTS indexer.indexed_modules +( + module_name TEXT NOT NULL PRIMARY KEY +); + +CREATE TABLE IF NOT EXISTS block +( + block_number BIGINT NOT NULL PRIMARY KEY, + header JSONB NULL +); + +CREATE TABLE IF NOT EXISTS tx +( + id BIGSERIAL PRIMARY KEY, + block_number BIGINT NOT NULL REFERENCES block (block_number), + tx_index BIGINT NOT NULL, + data JSONB NOT NULL +); + +CREATE TABLE IF NOT EXISTS event +( + id BIGSERIAL PRIMARY KEY, + block_number BIGINT NOT NULL REFERENCES block (block_number), + tx_index BIGINT NOT NULL REFERENCES tx (tx_index), + msg_idx BIGINT NOT NULL, + event_idx BIGINT NOT NULL, + type TEXT NOT NULL, + data JSONB NOT NULL +); + + diff --git a/indexer/postgres/col_type.go b/indexer/postgres/col_type.go deleted file mode 100644 index fe7d4b0aa260..000000000000 --- a/indexer/postgres/col_type.go +++ /dev/null @@ -1,54 +0,0 @@ -package postgres - -import ( - "fmt" - - indexerbase "cosmossdk.io/indexer/base" -) - -func columnType(field indexerbase.Field) (string, error) { - switch field.Kind { - case indexerbase.StringKind: - return "TEXT", nil - case indexerbase.BoolKind: - return "BOOLEAN", nil - case indexerbase.BytesKind: - return "BYTEA", nil - case indexerbase.Int8Kind: - return "SMALLINT", nil - case indexerbase.Int16Kind: - return "SMALLINT", nil - case indexerbase.Int32Kind: - return "INTEGER", nil - case indexerbase.Int64Kind: - return "BIGINT", nil - case indexerbase.Uint8Kind: - return "SMALLINT", nil - case indexerbase.Uint16Kind: - return "INTEGER", nil - case indexerbase.Uint32Kind: - return "BIGINT", nil - case indexerbase.Uint64Kind: - return "NUMERIC", nil - case indexerbase.IntegerKind: - return "NUMERIC", nil - case indexerbase.DecimalKind: - return "NUMERIC", nil - case indexerbase.Float32Kind: - return "REAL", nil - case indexerbase.Float64Kind: - return "DOUBLE PRECISION", nil - case indexerbase.EnumKind: - return "TEXT", nil // TODO: create enum type - case indexerbase.JSONKind: - return "JSONB", nil - case indexerbase.Bech32AddressKind: - return "TEXT", nil - case indexerbase.TimeKind: - return "TIMESTAMPTZ", nil // TODO: preserve nanos, one idea store as integer and then have generated TIMESTAMPTZ column - case indexerbase.DurationKind: - return "INTERVAL", nil // TODO: preserve nanos, see above - default: - return "", fmt.Errorf("unsupported kind %v for field %s", field.Kind, field.Name) - } -} diff --git a/indexer/postgres/column.go b/indexer/postgres/column.go new file mode 100644 index 000000000000..05ec015812de --- /dev/null +++ b/indexer/postgres/column.go @@ -0,0 +1,116 @@ +package postgres + +import ( + "fmt" + "io" + + "cosmossdk.io/schema" +) + +func (tm *TableManager) createColumnDef(writer io.Writer, field schema.Field) error { + _, err := fmt.Fprintf(writer, "%q ", field.Name) + if err != nil { + return err + } + + simple := simpleColumnType(field.Kind) + if simple != "" { + _, err = fmt.Fprintf(writer, "%s", simple) + if err != nil { + return err + } + + return writeNullability(writer, field.Nullable) + } else { + switch field.Kind { + case schema.EnumKind: + _, err = fmt.Fprintf(writer, "%q", enumTypeName(tm.moduleName, field.EnumDefinition)) + if err != nil { + return err + } + case schema.Bech32AddressKind: + _, err = fmt.Fprintf(writer, "TEXT") + if err != nil { + return err + } + + case schema.TimeKind: + nanosCol := fmt.Sprintf("%s_nanos", field.Name) + // TODO: retain at least microseconds in the timestamp + _, err = fmt.Fprintf(writer, "TIMESTAMPTZ GENERATED ALWAYS AS (to_timestamp(%q / 1000000000)) STORED,\n\t", nanosCol) + if err != nil { + return err + } + + _, err = fmt.Fprintf(writer, `%q BIGINT`, nanosCol) + if err != nil { + return err + } + default: + return fmt.Errorf("unexpected kind: %v, this should have been handled earlier", field.Kind) + } + + return writeNullability(writer, field.Nullable) + } +} + +func writeNullability(writer io.Writer, nullable bool) error { + if nullable { + _, err := fmt.Fprintf(writer, " NULL,\n\t") + return err + } else { + _, err := fmt.Fprintf(writer, " NOT NULL,\n\t") + return err + } +} + +func simpleColumnType(kind schema.Kind) string { + switch kind { + case schema.StringKind: + return "TEXT" + case schema.BoolKind: + return "BOOLEAN" + case schema.BytesKind: + return "BYTEA" + case schema.Int8Kind: + return "SMALLINT" + case schema.Int16Kind: + return "SMALLINT" + case schema.Int32Kind: + return "INTEGER" + case schema.Int64Kind: + return "BIGINT" + case schema.Uint8Kind: + return "SMALLINT" + case schema.Uint16Kind: + return "INTEGER" + case schema.Uint32Kind: + return "BIGINT" + case schema.Uint64Kind: + return "NUMERIC" + case schema.IntegerStringKind: + return "NUMERIC" + case schema.DecimalStringKind: + return "NUMERIC" + case schema.Float32Kind: + return "REAL" + case schema.Float64Kind: + return "DOUBLE PRECISION" + case schema.JSONKind: + return "JSONB" + case schema.DurationKind: + // TODO: set COMMENT on field indicating nanoseconds unit + return "BIGINT" // TODO: maybe convert to postgres interval + default: + return "" + } +} + +func (tm *TableManager) updatableColumnName(field schema.Field) (name string, err error) { + name = field.Name + if field.Kind == schema.TimeKind { + name = fmt.Sprintf("%s_nanos", name) + } + name = fmt.Sprintf("%q", name) + return +} diff --git a/indexer/postgres/count.go b/indexer/postgres/count.go new file mode 100644 index 000000000000..d5b4c8eb5b08 --- /dev/null +++ b/indexer/postgres/count.go @@ -0,0 +1,14 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" +) + +func (tm *TableManager) Count(ctx context.Context, tx *sql.Tx) (int, error) { + row := tx.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %q;", tm.TableName())) + var count int + err := row.Scan(&count) + return count, err +} diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index cddb29bf0630..477548d84347 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -6,42 +6,51 @@ import ( "fmt" "io" "strings" - - indexerbase "cosmossdk.io/indexer/base" ) -func (s *TableManager) CreateTable(ctx context.Context, tx *sql.Tx) error { +func (tm *TableManager) CreateTable(ctx context.Context, tx *sql.Tx) error { buf := new(strings.Builder) - err := s.GenCreateTable(buf) + err := tm.CreateTableSql(buf) if err != nil { return err } - _, err = tx.ExecContext(ctx, buf.String()) + sqlStr := buf.String() + tm.options.Logger.Debug("Creating table", "table", tm.TableName(), "sql", sqlStr) + _, err = tx.ExecContext(ctx, sqlStr) return err } -// GenCreateTable generates a CREATE TABLE statement for the object type. -func (s *TableManager) GenCreateTable(writer io.Writer) error { - _, err := fmt.Fprintf(writer, "CREATE TABLE IF NOT EXISTS %s (", s.TableName()) +// CreateTableSql generates a CREATE TABLE statement for the object type. +func (tm *TableManager) CreateTableSql(writer io.Writer) error { + _, err := fmt.Fprintf(writer, "CREATE TABLE IF NOT EXISTS %q (", tm.TableName()) if err != nil { return err } isSingleton := false - if len(s.typ.KeyFields) == 0 { + if len(tm.typ.KeyFields) == 0 { isSingleton = true _, err = fmt.Fprintf(writer, "_id INTEGER NOT NULL CHECK (_id = 1),\n\t") } else { - for _, field := range s.typ.KeyFields { - err = s.createColumnDef(writer, field) + for _, field := range tm.typ.KeyFields { + err = tm.createColumnDef(writer, field) if err != nil { return err } } } - for _, field := range s.typ.ValueFields { - err = s.createColumnDef(writer, field) + for _, field := range tm.typ.ValueFields { + err = tm.createColumnDef(writer, field) + if err != nil { + return err + } + } + + // add _deleted column when we have RetainDeletions set and enabled + // NOTE: needs more design + if tm.options.RetainDeletions && tm.typ.RetainDeletions { + _, err = fmt.Fprintf(writer, "_deleted BOOLEAN NOT NULL DEFAULT FALSE,\n\t") if err != nil { return err } @@ -49,47 +58,49 @@ func (s *TableManager) GenCreateTable(writer io.Writer) error { var pKeys []string if !isSingleton { - for _, field := range s.typ.KeyFields { - pKeys = append(pKeys, field.Name) + for _, field := range tm.typ.KeyFields { + name, err := tm.updatableColumnName(field) + if err != nil { + return err + } + + pKeys = append(pKeys, name) } } else { pKeys = []string{"_id"} } - _, err = fmt.Fprintf(writer, "PRIMARY KEY (%s)\n", strings.Join(pKeys, ", ")) + _, err = fmt.Fprintf(writer, "PRIMARY KEY (%s)", strings.Join(pKeys, ", ")) if err != nil { return err } - _, err = fmt.Fprintf(writer, `); + // TODO: we need test data to not generate constraint failures to safely enable this + //for _, uniq := range tm.typ.UniqueConstraints { + // cols := make([]string, len(uniq.FieldNames)) + // for i, name := range uniq.FieldNames { + // field, ok := tm.allFields[name] + // if !ok { + // return fmt.Errorf("unknown field %q in unique constraint", name) + // } + // + // cols[i], err = tm.updatableColumnName(field) + // if err != nil { + // return err + // } + // } + // + // _, err = fmt.Fprintf(writer, ",\n\tUNIQUE NULLS NOT DISTINCT (%s)", strings.Join(cols, ", ")) + //} -GRANT SELECT ON TABLE %s TO PUBLIC; -`, s.TableName()) - if err != nil { - return err - } + _, err = fmt.Fprintf(writer, ` +); - return nil -} - -func (s *TableManager) createColumnDef(writer io.Writer, field indexerbase.Field) error { - typeStr, err := columnType(field) +GRANT SELECT ON TABLE %q TO PUBLIC; +`, tm.TableName()) if err != nil { return err } - _, err = fmt.Fprintf(writer, "%s %s", field.Name, typeStr) - if err != nil { - return err - } - - if !field.Nullable { - _, err = fmt.Fprint(writer, " NOT") - if err != nil { - return err - } - } - - _, err = fmt.Fprint(writer, " NULL,\n\t") - return err + return nil } diff --git a/indexer/postgres/delete.go b/indexer/postgres/delete.go new file mode 100644 index 000000000000..d6ad911d42bb --- /dev/null +++ b/indexer/postgres/delete.go @@ -0,0 +1,58 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "io" + "strings" +) + +func (tm *TableManager) Delete(ctx context.Context, tx *sql.Tx, key interface{}) error { + buf := new(strings.Builder) + var params []interface{} + var err error + if tm.options.RetainDeletions && tm.typ.RetainDeletions { + params, err = tm.RetainDeleteSqlAndParams(buf, key) + } else { + params, err = tm.DeleteSqlAndParams(buf, key) + } + if err != nil { + return err + } + + sqlStr := buf.String() + tm.options.Logger.Debug("Delete", "sql", sqlStr, "params", params) + _, err = tx.ExecContext(ctx, sqlStr, params...) + return err +} + +func (tm *TableManager) DeleteSqlAndParams(w io.Writer, key interface{}) ([]interface{}, error) { + _, err := fmt.Fprintf(w, "DELETE FROM %q", tm.TableName()) + if err != nil { + return nil, err + } + + _, keyParams, err := tm.WhereSqlAndParams(w, key, 1) + if err != nil { + return nil, err + } + + _, err = fmt.Fprintf(w, ";") + return keyParams, err +} + +func (tm *TableManager) RetainDeleteSqlAndParams(w io.Writer, key interface{}) ([]interface{}, error) { + _, err := fmt.Fprintf(w, "UPDATE %q SET _deleted = TRUE", tm.TableName()) + if err != nil { + return nil, err + } + + _, keyParams, err := tm.WhereSqlAndParams(w, key, 1) + if err != nil { + return nil, err + } + + _, err = fmt.Fprintf(w, ";") + return keyParams, err +} diff --git a/indexer/postgres/enum.go b/indexer/postgres/enum.go new file mode 100644 index 000000000000..49cbf8b740b5 --- /dev/null +++ b/indexer/postgres/enum.go @@ -0,0 +1,86 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "io" + "strings" + + "cosmossdk.io/schema" +) + +func (m *ModuleManager) createEnumTypesForFields(ctx context.Context, tx *sql.Tx, fields []schema.Field) error { + for _, field := range fields { + if field.Kind != schema.EnumKind { + continue + } + + if _, ok := m.definedEnums[field.EnumDefinition.Name]; ok { + // if the enum type is already defined, skip + // we assume validation already happened + continue + } + + err := m.CreateEnumType(ctx, tx, field.EnumDefinition) + if err != nil { + return err + } + + m.definedEnums[field.EnumDefinition.Name] = field.EnumDefinition + } + + return nil +} + +func enumTypeName(moduleName string, enum schema.EnumDefinition) string { + return fmt.Sprintf("%s_%s", moduleName, enum.Name) +} + +func (m *ModuleManager) CreateEnumType(ctx context.Context, tx *sql.Tx, enum schema.EnumDefinition) error { + typeName := enumTypeName(m.moduleName, enum) + row := tx.QueryRowContext(ctx, "SELECT 1 FROM pg_type WHERE typname = $1", typeName) + var res interface{} + if err := row.Scan(&res); err != nil { + if err != sql.ErrNoRows { + return fmt.Errorf("failed to check if enum type %q exists: %w", typeName, err) + } + } else { + // the enum type already exists + return nil + } + + buf := new(strings.Builder) + err := m.CreateEnumTypeSql(buf, enum) + if err != nil { + return err + } + + sqlStr := buf.String() + m.options.Logger.Debug("Creating enum type", "sql", sqlStr) + _, err = tx.ExecContext(ctx, sqlStr) + return err +} + +func (m *ModuleManager) CreateEnumTypeSql(writer io.Writer, enum schema.EnumDefinition) error { + _, err := fmt.Fprintf(writer, "CREATE TYPE %q AS ENUM (", enumTypeName(m.moduleName, enum)) + if err != nil { + return err + } + + for i, value := range enum.Values { + if i > 0 { + _, err = fmt.Fprintf(writer, ", ") + if err != nil { + return err + } + } + _, err = fmt.Fprintf(writer, "'%s'", value) + if err != nil { + return err + } + } + + _, err = fmt.Fprintf(writer, ");") + return err +} diff --git a/indexer/postgres/go.mod b/indexer/postgres/go.mod index 353f37046e2f..dc0615cf41b6 100644 --- a/indexer/postgres/go.mod +++ b/indexer/postgres/go.mod @@ -4,12 +4,13 @@ module cosmossdk.io/indexer/postgres // with older codebases. go 1.12 -require ( - // NOTE: cosmossdk.io/indexer/base should be the only dependency here - // so there are no problems building this with any version of the SDK. - // This module should only use the golang standard library (database/sql) - // and cosmossdk.io/indexer/base. - cosmossdk.io/indexer/base v0.0.0-00010101000000-000000000000 -) - -replace cosmossdk.io/indexer/base => ../base \ No newline at end of file +// NOTE: cosmossdk.io/schema should be the only dependency here +// so there are no problems building this with any version of the SDK. +// This module should only use the golang standard library (database/sql) +// and cosmossdk.io/indexer/base. +require cosmossdk.io/schema v0.0.0 + +// TODO: is this dependency okay? +require github.com/cosmos/btcutil v1.0.5 + +replace cosmossdk.io/schema => ../../schema diff --git a/indexer/postgres/go.sum b/indexer/postgres/go.sum index 62fbcdb3a541..fa4b37f95d3b 100644 --- a/indexer/postgres/go.sum +++ b/indexer/postgres/go.sum @@ -1,12 +1,2 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -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= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= diff --git a/indexer/postgres/graphql-proxy/.gitignore b/indexer/postgres/graphql-proxy/.gitignore new file mode 100644 index 000000000000..16d54bb13c8a --- /dev/null +++ b/indexer/postgres/graphql-proxy/.gitignore @@ -0,0 +1,24 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ diff --git a/indexer/postgres/graphql-proxy/README.md b/indexer/postgres/graphql-proxy/README.md new file mode 100644 index 000000000000..e34a99b446b4 --- /dev/null +++ b/indexer/postgres/graphql-proxy/README.md @@ -0,0 +1,47 @@ +# Astro Starter Kit: Minimal + +```sh +npm create astro@latest -- --template minimal +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json) + +> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +```text +/ +├── public/ +├── src/ +│ └── pages/ +│ └── index.astro +└── package.json +``` + +Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. + +There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. + +Any static assets, like images, can be placed in the `public/` directory. + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## 👀 Want to learn more? + +Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/indexer/postgres/graphql-proxy/astro.config.mjs b/indexer/postgres/graphql-proxy/astro.config.mjs new file mode 100644 index 000000000000..37b2bbe8c702 --- /dev/null +++ b/indexer/postgres/graphql-proxy/astro.config.mjs @@ -0,0 +1,6 @@ +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + output: 'server' +}); diff --git a/indexer/postgres/graphql-proxy/package.json b/indexer/postgres/graphql-proxy/package.json new file mode 100644 index 000000000000..df3349512273 --- /dev/null +++ b/indexer/postgres/graphql-proxy/package.json @@ -0,0 +1,19 @@ +{ + "name": "graphql-proxy", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro check && astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/check": "^0.7.0", + "@types/pg": "^8.11.6", + "astro": "^4.11.0", + "pg": "^8.12.0", + "typescript": "^5.5.2" + } +} \ No newline at end of file diff --git a/indexer/postgres/graphql-proxy/pnpm-lock.yaml b/indexer/postgres/graphql-proxy/pnpm-lock.yaml new file mode 100644 index 000000000000..f8ed5b7be957 --- /dev/null +++ b/indexer/postgres/graphql-proxy/pnpm-lock.yaml @@ -0,0 +1,4311 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@astrojs/check': + specifier: ^0.7.0 + version: 0.7.0(typescript@5.5.2) + '@types/pg': + specifier: ^8.11.6 + version: 8.11.6 + astro: + specifier: ^4.11.0 + version: 4.11.0(@types/node@20.14.7)(typescript@5.5.2) + pg: + specifier: ^8.12.0 + version: 8.12.0 + typescript: + specifier: ^5.5.2 + version: 5.5.2 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@astrojs/check@0.7.0': + resolution: {integrity: sha512-UTqwOeKNu9IYZmJXEeWnQuTdSd/pX58Hl4TUARsMlT97SVDL//kLBE4T/ctxRz6J573N87oE5ddtW/uOOnQTug==} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + + '@astrojs/compiler@2.8.0': + resolution: {integrity: sha512-yrpD1WRGqsJwANaDIdtHo+YVjvIOFAjC83lu5qENIgrafwZcJgSXDuwVMXOgok4tFzpeKLsFQ6c3FoUdloLWBQ==} + + '@astrojs/internal-helpers@0.4.0': + resolution: {integrity: sha512-6B13lz5n6BrbTqCTwhXjJXuR1sqiX/H6rTxzlXx+lN1NnV4jgnq/KJldCQaUWJzPL5SiWahQyinxAbxQtwgPHA==} + + '@astrojs/language-server@2.10.0': + resolution: {integrity: sha512-crHXpqYfA5qWioiuZnZFpTsNItgBlF1f0S9MzDYS7/pfCALkHNJ7K3w9U/j0uMKymsT4hC7BfMaX0DYlfdSzHg==} + hasBin: true + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-astro: '>=0.11.0' + peerDependenciesMeta: + prettier: + optional: true + prettier-plugin-astro: + optional: true + + '@astrojs/markdown-remark@5.1.0': + resolution: {integrity: sha512-S6Z3K2hOB7MfjeDoHsotnP/q2UsnEDB8NlNAaCjMDsGBZfTUbWxyLW3CaphEWw08f6KLZi2ibK9yC3BaMhh2NQ==} + + '@astrojs/prism@3.1.0': + resolution: {integrity: sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} + + '@astrojs/telemetry@3.1.0': + resolution: {integrity: sha512-/ca/+D8MIKEC8/A9cSaPUqQNZm+Es/ZinRv0ZAzvu2ios7POQSsVD+VOj7/hypWNsNM3T7RpfgNq7H2TU1KEHA==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} + + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.24.7': + resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.24.7': + resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.24.7': + resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.24.7': + resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.24.7': + resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-environment-visitor@7.24.7': + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-function-name@7.24.7': + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-hoist-variables@7.24.7': + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.24.7': + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.24.7': + resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.24.7': + resolution: {integrity: sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-simple-access@7.24.7': + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-split-export-declaration@7.24.7': + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.24.7': + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.24.7': + resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.24.7': + resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.24.7': + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.24.7': + resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.24.7': + resolution: {integrity: sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.24.7': + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.24.7': + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.24.7': + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} + engines: {node: '>=6.9.0'} + + '@emmetio/abbreviation@2.3.3': + resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} + + '@emmetio/css-abbreviation@2.1.8': + resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} + + '@emmetio/css-parser@0.4.0': + resolution: {integrity: sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw==} + + '@emmetio/html-matcher@1.3.0': + resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} + + '@emmetio/scanner@1.0.4': + resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} + + '@emmetio/stream-reader-utils@0.1.0': + resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} + + '@emmetio/stream-reader@2.2.0': + resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + + '@emnapi/runtime@1.2.0': + resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@img/sharp-darwin-arm64@0.33.4': + resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.4': + resolution: {integrity: sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.2': + resolution: {integrity: sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==} + engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.2': + resolution: {integrity: sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==} + engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.2': + resolution: {integrity: sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.2': + resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.2': + resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.2': + resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.2': + resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.2': + resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.4': + resolution: {integrity: sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.4': + resolution: {integrity: sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==} + engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.4': + resolution: {integrity: sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==} + engines: {glibc: '>=2.31', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.4': + resolution: {integrity: sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.4': + resolution: {integrity: sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.4': + resolution: {integrity: sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.4': + resolution: {integrity: sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.4': + resolution: {integrity: sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.4': + resolution: {integrity: sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [win32] + + '@johnsoncodehk/vscode-html-languageservice@5.2.0-34a5462': + resolution: {integrity: sha512-etqLfpSJ5zaw76KUNF603be6d6QsiQPmaHr9FKEp4zhLZJzWCCMH6Icak7MtLUFLZLMpL761mZNImi/joBo1ZA==} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rollup/rollup-android-arm-eabi@4.18.0': + resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.18.0': + resolution: {integrity: sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.18.0': + resolution: {integrity: sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.18.0': + resolution: {integrity: sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm-gnueabihf@4.18.0': + resolution: {integrity: sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.18.0': + resolution: {integrity: sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.18.0': + resolution: {integrity: sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.18.0': + resolution: {integrity: sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': + resolution: {integrity: sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.18.0': + resolution: {integrity: sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.18.0': + resolution: {integrity: sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.18.0': + resolution: {integrity: sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.18.0': + resolution: {integrity: sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.18.0': + resolution: {integrity: sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.18.0': + resolution: {integrity: sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.18.0': + resolution: {integrity: sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==} + cpu: [x64] + os: [win32] + + '@shikijs/core@1.9.0': + resolution: {integrity: sha512-cbSoY8P/jgGByG8UOl3jnP/CWg/Qk+1q+eAKWtcrU3pNoILF8wTsLB0jT44qUBV8Ce1SvA9uqcM9Xf+u3fJFBw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/nlcst@1.0.4': + resolution: {integrity: sha512-ABoYdNQ/kBSsLvZAekMhIPMQ3YUZvavStpKYs7BjLLuKVmIMA0LUgZ7b54zzuWJRbHF80v1cNf4r90Vd6eMQDg==} + + '@types/node@20.14.7': + resolution: {integrity: sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==} + + '@types/pg@8.11.6': + resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} + + '@types/unist@2.0.10': + resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} + + '@types/unist@3.0.2': + resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@volar/kit@2.2.5': + resolution: {integrity: sha512-Bmn0UCaT43xUGGRwcmFG9lKhiCCLjRT4ScSLLPn5C9ltUcSGnIFFDlbZZa1PreHYHq25/4zkXt9Ap32klAh17w==} + peerDependencies: + typescript: '*' + + '@volar/language-core@2.2.5': + resolution: {integrity: sha512-2htyAuxRrAgETmFeUhT4XLELk3LiEcqoW/B8YUXMF6BrGWLMwIR09MFaZYvrA2UhbdAeSyeQ726HaWSWkexUcQ==} + + '@volar/language-server@2.2.5': + resolution: {integrity: sha512-PV/jkUkI+m72HTXwnY7hsGqLY3VNi96ZRoWFRzVC9QG/853bixxjveXPJIiydMJ9I739lO3kcj3hnGrF5Sm+HA==} + + '@volar/language-service@2.2.5': + resolution: {integrity: sha512-a97e/0uCe+uSu23F4zvgvldqJtZe6jugQeEHWjTfhgOEO8+Be0t5CZNNVItQqmPyAsD8eElg0S/cP6uxvCmCSQ==} + + '@volar/snapshot-document@2.2.5': + resolution: {integrity: sha512-MTOvWVKxM7ugKO3Amffkv2pND03fe2JtfygYaputqjVFML7YxtTXj8SPnI2pODLeSwOKzDYL6Q8r5j6Y5AgUzQ==} + + '@volar/source-map@2.2.5': + resolution: {integrity: sha512-wrOEIiZNf4E+PWB0AxyM4tfhkfldPsb3bxg8N6FHrxJH2ohar7aGu48e98bp3pR9HUA7P/pR9VrLmkTrgCCnWQ==} + + '@volar/typescript@2.2.5': + resolution: {integrity: sha512-eSV/n75+ppfEVugMC/salZsI44nXDPAyL6+iTYCNLtiLHGJsnMv9GwiDMujrvAUj/aLQyqRJgYtXRoxop2clCw==} + + '@vscode/emmet-helper@2.9.3': + resolution: {integrity: sha512-rB39LHWWPQYYlYfpv9qCoZOVioPCftKXXqrsyqN1mTWZM6dTnONT63Db+03vgrBbHzJN45IrgS/AGxw9iiqfEw==} + + '@vscode/l10n@0.0.16': + resolution: {integrity: sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==} + + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + + acorn@8.12.0: + resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astro@4.11.0: + resolution: {integrity: sha512-3VWxz/08sChQIX68tuE7Y769DUdjsT3Zq2/y4SkrDRlwN9IZ/aebwcRWr5a2yMSdO2vpFxtEdobq0mKnMlLErg==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + + axobject-query@4.0.0: + resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boxen@7.1.1: + resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} + engines: {node: '>=14.16'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.23.1: + resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + + caniuse-lite@1.0.30001636: + resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + ci-info@4.0.0: + resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} + engines: {node: '>=8'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + deterministic-object-hash@2.0.2: + resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} + engines: {node: '>=18'} + + devalue@5.0.0: + resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dset@3.1.3: + resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} + engines: {node: '>=4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.4.808: + resolution: {integrity: sha512-0ItWyhPYnww2VOuCGF4s1LTfbrdAV2ajy/TN+ZTuhR23AHI6rWHCrBXJ/uxoXOvRRqw8qjYVrG81HFI7x/2wdQ==} + + emmet@2.4.7: + resolution: {integrity: sha512-O5O5QNqtdlnQM2bmKHtJgyChcrFMgQuulI+WdiOw2NArzprUqqxUW6bgYtKvzKgrsYpuLWalOkdhNP+1jluhCA==} + + emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-module-lexer@1.5.3: + resolution: {integrity: sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-yarn-workspace-root2@1.2.16: + resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-from-html@2.0.1: + resolution: {integrity: sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==} + + hast-util-from-parse5@8.0.1: + resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.0.4: + resolution: {integrity: sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==} + + hast-util-to-html@9.0.1: + resolution: {integrity: sha512-hZOofyZANbyWo+9RP75xIDV/gq+OUKx+T46IlwERnKmfpwp81XBFbT9mi26ws+SJchA4RVUQwIBJpqEOBhMzEQ==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@8.0.0: + resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-core-module@2.14.0: + resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@2.3.1: + resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + load-yaml-file@0.2.0: + resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + + markdown-table@3.0.3: + resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + + mdast-util-from-markdown@2.0.1: + resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} + + mdast-util-gfm-autolink-literal@2.0.0: + resolution: {integrity: sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==} + + mdast-util-gfm-footnote@2.0.0: + resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.0.0: + resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.0: + resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.1: + resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==} + + micromark-extension-gfm-autolink-literal@2.0.0: + resolution: {integrity: sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==} + + micromark-extension-gfm-footnote@2.0.0: + resolution: {integrity: sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==} + + micromark-extension-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==} + + micromark-extension-gfm-table@2.0.0: + resolution: {integrity: sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.0.1: + resolution: {integrity: sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.0: + resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} + + micromark-factory-label@2.0.0: + resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} + + micromark-factory-space@2.0.0: + resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} + + micromark-factory-title@2.0.0: + resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} + + micromark-factory-whitespace@2.0.0: + resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} + + micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + + micromark-util-chunked@2.0.0: + resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} + + micromark-util-classify-character@2.0.0: + resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} + + micromark-util-combine-extensions@2.0.0: + resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} + + micromark-util-decode-numeric-character-reference@2.0.1: + resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} + + micromark-util-decode-string@2.0.0: + resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} + + micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + micromark-util-html-tag-name@2.0.0: + resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} + + micromark-util-normalize-identifier@2.0.0: + resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} + + micromark-util-resolve-all@2.0.0: + resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} + + micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + + micromark-util-subtokenize@2.0.1: + resolution: {integrity: sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==} + + micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + micromark@4.0.0: + resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nlcst-to-string@3.1.1: + resolution: {integrity: sha512-63mVyqaqt0cmn2VcI2aH6kxe1rLAmSROqHMA0i4qqg1tidkfExgpb0FGMikMCn86mw5dFtBtEANfmSSK7TjNHw==} + + node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + ora@8.0.1: + resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} + engines: {node: '>=18'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-queue@8.0.1: + resolution: {integrity: sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==} + engines: {node: '>=18'} + + p-timeout@6.1.2: + resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} + engines: {node: '>=14.16'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parse-latin@5.0.1: + resolution: {integrity: sha512-b/K8ExXaWC9t34kKeDV8kGXBkXZ1HCSAZRYE7HR14eA1GlXX5L8iWhs8USJNhQU9q5ci413jCKF0gOyovvyRBg==} + + parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.6.4: + resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.6.2: + resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.6.1: + resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + + pg@8.12.0: + resolution: {integrity: sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + + preferred-pm@3.1.3: + resolution: {integrity: sha512-MkXsENfftWSRpzCzImcp4FRsCc3y1opwB73CfCNWyzMqArju2CrlMHlqB7VexKiPEOjGMbttv1r9fSCn5S610w==} + engines: {node: '>=10'} + + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + rehype-parse@9.0.0: + resolution: {integrity: sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-stringify@10.0.0: + resolution: {integrity: sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==} + + rehype@13.0.1: + resolution: {integrity: sha512-AcSLS2mItY+0fYu9xKxOu1LhUZeBZZBx8//5HKzF+0XP+eP8+6a5MXn2+DW2kfXR6Dtp1FEXMVrjyKAcvcU8vg==} + + remark-gfm@4.0.0: + resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.0: + resolution: {integrity: sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==} + + remark-smartypants@2.1.0: + resolution: {integrity: sha512-qoF6Vz3BjU2tP6OfZqHOvCU0ACmu/6jhGaINSQRI9mM7wCxNQTKB3JUAN4SVoN2ybElEDTxBIABRep7e569iJw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + retext-latin@3.1.0: + resolution: {integrity: sha512-5MrD1tuebzO8ppsja5eEu+ZbBeUNCjoEarn70tkXOS7Bdsdf6tNahsv2bY0Z8VooFF6cw7/6S+d3yI/TMlMVVQ==} + + retext-smartypants@5.2.0: + resolution: {integrity: sha512-Do8oM+SsjrbzT2UNIKgheP0hgUQTDDQYyZaIY3kfq0pdFzoPk+ZClYJ+OERNXveog4xf1pZL4PfRxNoVL7a/jw==} + + retext-stringify@3.1.0: + resolution: {integrity: sha512-767TLOaoXFXyOnjx/EggXlb37ZD2u4P1n0GJqVdpipqACsQP+20W+BNpMYrlJkq7hxffnFk+jc6mAK9qrbuB8w==} + + retext@8.1.0: + resolution: {integrity: sha512-N9/Kq7YTn6ZpzfiGW45WfEGJqFf1IM1q8OsRa1CGzIebCJBNCANDRmOrholiDRGKo/We7ofKR4SEvcGAWEMD3Q==} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.18.0: + resolution: {integrity: sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.33.4: + resolution: {integrity: sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==} + engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shiki@1.9.0: + resolution: {integrity: sha512-i6//Lqgn7+7nZA0qVjoYH0085YdNk4MC+tJV4bo+HgjgRMJ0JmkLZzFAuvVioJqLkcGDK5GAMpghZEZkCnwxpQ==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.1.0: + resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==} + engines: {node: '>=18'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tsconfck@3.1.0: + resolution: {integrity: sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + + typescript-auto-import-cache@0.3.3: + resolution: {integrity: sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA==} + + typescript@5.5.2: + resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + unherit@3.0.1: + resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==} + + unified@10.1.2: + resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@5.2.1: + resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-modify-children@3.1.1: + resolution: {integrity: sha512-yXi4Lm+TG5VG+qvokP6tpnk+r1EPwyYL04JWDxLvgvPV40jANh7nm3udk65OOWquvbMDe+PL9+LmkxDpTv/7BA==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@3.0.3: + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@2.0.2: + resolution: {integrity: sha512-+LWpMFqyUwLGpsQxpumsQ9o9DG2VGLFrpz+rpVXYIEdPy57GSy5HioC0g3bg/8WP9oCLlapQtklOzQ8uLS496Q==} + + unist-util-visit-parents@5.1.3: + resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@4.1.2: + resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + update-browserslist-db@1.0.16: + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vfile-location@5.0.2: + resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} + + vfile-message@3.1.4: + resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@5.3.7: + resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} + + vfile@6.0.1: + resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} + + vite@5.3.1: + resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitefu@0.2.5: + resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + vite: + optional: true + + volar-service-css@0.0.45: + resolution: {integrity: sha512-f+AlUI1+kESbcZSVaNJVAnK0c/9Da5StoxzPqA5/8VqUHJWNdubWNnwG5xpFVTfgh6pgTcey3UBhBfHytFaIOg==} + peerDependencies: + '@volar/language-service': ~2.2.3 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-emmet@0.0.45: + resolution: {integrity: sha512-9nLXSDkR1vA/3fQkFEsSXAu3XovQxOpTkVG2jilQgfek/K1ZLkaA/WMhN/TtmPmQg4NxE9Ni6mA5udBQ5gVXIA==} + peerDependencies: + '@volar/language-service': ~2.2.3 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-html@0.0.45: + resolution: {integrity: sha512-tLTJqfy1v5C4nmeAsfekFIKPl4r4qDMyL0L9MWywr/EApZzPCsbeUGxCqdzxSMC2q7PMCfX2i167txDo+J0LVA==} + peerDependencies: + '@volar/language-service': ~2.2.3 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-prettier@0.0.45: + resolution: {integrity: sha512-+mBS2EsDgp/kunKEBnHvhBwIQm5v2ahw4NKpKdg4sTpXy3UxqHt+Fq/wRYQ7Z8LlNVNRVfp75ThjM+w2zaZBAw==} + peerDependencies: + '@volar/language-service': ~2.2.3 + prettier: ^2.2 || ^3.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + prettier: + optional: true + + volar-service-typescript-twoslash-queries@0.0.45: + resolution: {integrity: sha512-KrPUUvKggZgV9mrDpstCzmf20irgv0ooMv+FGDzIIQUkya+d2+nSS8Mx2h9FvsYgLccUVw5jU3Rhwhd3pv/7qg==} + peerDependencies: + '@volar/language-service': ~2.2.3 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-typescript@0.0.45: + resolution: {integrity: sha512-i/mMIIAMastJ2kgPo3qvX0Rrl7NyxhIYZ0ug/B4ambZcLPI1vzBgS2fmvyWX3jhBYHh8NmbAotFj+0Y9JtN47A==} + peerDependencies: + '@volar/language-service': ~2.2.3 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-css-languageservice@6.2.14: + resolution: {integrity: sha512-5UPQ9Y1sUTnuMyaMBpO7LrBkqjhEJb5eAwdUlDp+Uez8lry+Tspnk3+3p2qWS4LlNsr4p3v9WkZxUf1ltgFpgw==} + + vscode-html-languageservice@5.2.0: + resolution: {integrity: sha512-cdNMhyw57/SQzgUUGSIMQ66jikqEN6nBNyhx5YuOyj9310+eY9zw8Q0cXpiKzDX8aHYFewQEXRnigl06j/TVwQ==} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.11: + resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-nls@5.2.0: + resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} + + vscode-uri@2.1.2: + resolution: {integrity: sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==} + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + which-pm@2.0.0: + resolution: {integrity: sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==} + engines: {node: '>=8.15'} + + which-pm@2.2.0: + resolution: {integrity: sha512-MOiaDbA5ZZgUjkeMWM5EkJp4loW5ZRoa5bc3/aeMox/PJelMhE6t7S/mLuiY43DBupyxH+S0U1bTui9kWUlmsw==} + engines: {node: '>=8.15'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + + zod-to-json-schema@3.23.1: + resolution: {integrity: sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==} + peerDependencies: + zod: ^3.23.3 + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@astrojs/check@0.7.0(typescript@5.5.2)': + dependencies: + '@astrojs/language-server': 2.10.0(typescript@5.5.2) + chokidar: 3.6.0 + fast-glob: 3.3.2 + kleur: 4.1.5 + typescript: 5.5.2 + yargs: 17.7.2 + transitivePeerDependencies: + - prettier + - prettier-plugin-astro + + '@astrojs/compiler@2.8.0': {} + + '@astrojs/internal-helpers@0.4.0': {} + + '@astrojs/language-server@2.10.0(typescript@5.5.2)': + dependencies: + '@astrojs/compiler': 2.8.0 + '@jridgewell/sourcemap-codec': 1.4.15 + '@volar/kit': 2.2.5(typescript@5.5.2) + '@volar/language-core': 2.2.5 + '@volar/language-server': 2.2.5 + '@volar/language-service': 2.2.5 + '@volar/typescript': 2.2.5 + fast-glob: 3.3.2 + volar-service-css: 0.0.45(@volar/language-service@2.2.5) + volar-service-emmet: 0.0.45(@volar/language-service@2.2.5) + volar-service-html: 0.0.45(@volar/language-service@2.2.5) + volar-service-prettier: 0.0.45(@volar/language-service@2.2.5) + volar-service-typescript: 0.0.45(@volar/language-service@2.2.5) + volar-service-typescript-twoslash-queries: 0.0.45(@volar/language-service@2.2.5) + vscode-html-languageservice: 5.2.0 + vscode-uri: 3.0.8 + transitivePeerDependencies: + - typescript + + '@astrojs/markdown-remark@5.1.0': + dependencies: + '@astrojs/prism': 3.1.0 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.1 + hast-util-to-text: 4.0.2 + import-meta-resolve: 4.1.0 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.0 + remark-gfm: 4.0.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.0 + remark-smartypants: 2.1.0 + shiki: 1.9.0 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.1 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@3.1.0': + dependencies: + prismjs: 1.29.0 + + '@astrojs/telemetry@3.1.0': + dependencies: + ci-info: 4.0.0 + debug: 4.3.5 + dlv: 1.1.3 + dset: 3.1.3 + is-docker: 3.0.0 + is-wsl: 3.1.0 + which-pm-runs: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/compat-data@7.24.7': {} + + '@babel/core@7.24.7': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helpers': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + convert-source-map: 2.0.0 + debug: 4.3.5 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.24.7': + dependencies: + '@babel/types': 7.24.7 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + '@babel/helper-annotate-as-pure@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/helper-compilation-targets@7.24.7': + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + browserslist: 4.23.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-environment-visitor@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/helper-function-name@7.24.7': + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + + '@babel/helper-hoist-variables@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/helper-module-imports@7.24.7': + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.24.7': {} + + '@babel/helper-simple-access@7.24.7': + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-split-export-declaration@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/helper-string-parser@7.24.7': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/helper-validator-option@7.24.7': {} + + '@babel/helpers@7.24.7': + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@babel/parser@7.24.7': + dependencies: + '@babel/types': 7.24.7 + + '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-react-jsx@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + + '@babel/template@7.24.7': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + + '@babel/traverse@7.24.7': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.24.7': + dependencies: + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@emmetio/abbreviation@2.3.3': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-abbreviation@2.1.8': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-parser@0.4.0': + dependencies: + '@emmetio/stream-reader': 2.2.0 + '@emmetio/stream-reader-utils': 0.1.0 + + '@emmetio/html-matcher@1.3.0': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/scanner@1.0.4': {} + + '@emmetio/stream-reader-utils@0.1.0': {} + + '@emmetio/stream-reader@2.2.0': {} + + '@emnapi/runtime@1.2.0': + dependencies: + tslib: 2.6.3 + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@img/sharp-darwin-arm64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.2 + optional: true + + '@img/sharp-darwin-x64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.2 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.2': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.2': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.2': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.2': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.2': + optional: true + + '@img/sharp-linux-arm64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.2 + optional: true + + '@img/sharp-linux-arm@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.2 + optional: true + + '@img/sharp-linux-s390x@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.2 + optional: true + + '@img/sharp-linux-x64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.2 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + optional: true + + '@img/sharp-wasm32@0.33.4': + dependencies: + '@emnapi/runtime': 1.2.0 + optional: true + + '@img/sharp-win32-ia32@0.33.4': + optional: true + + '@img/sharp-win32-x64@0.33.4': + optional: true + + '@johnsoncodehk/vscode-html-languageservice@5.2.0-34a5462': + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.11 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.4.15': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@rollup/rollup-android-arm-eabi@4.18.0': + optional: true + + '@rollup/rollup-android-arm64@4.18.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.18.0': + optional: true + + '@rollup/rollup-darwin-x64@4.18.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.18.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.18.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.18.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.18.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.18.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.18.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.18.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.18.0': + optional: true + + '@shikijs/core@1.9.0': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.24.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.24.7 + + '@types/cookie@0.6.0': {} + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/estree@1.0.5': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.2 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.2 + + '@types/ms@0.7.34': {} + + '@types/nlcst@1.0.4': + dependencies: + '@types/unist': 2.0.10 + + '@types/node@20.14.7': + dependencies: + undici-types: 5.26.5 + + '@types/pg@8.11.6': + dependencies: + '@types/node': 20.14.7 + pg-protocol: 1.6.1 + pg-types: 4.0.2 + + '@types/unist@2.0.10': {} + + '@types/unist@3.0.2': {} + + '@ungap/structured-clone@1.2.0': {} + + '@volar/kit@2.2.5(typescript@5.5.2)': + dependencies: + '@volar/language-service': 2.2.5 + '@volar/typescript': 2.2.5 + typesafe-path: 0.2.2 + typescript: 5.5.2 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + + '@volar/language-core@2.2.5': + dependencies: + '@volar/source-map': 2.2.5 + + '@volar/language-server@2.2.5': + dependencies: + '@volar/language-core': 2.2.5 + '@volar/language-service': 2.2.5 + '@volar/snapshot-document': 2.2.5 + '@volar/typescript': 2.2.5 + '@vscode/l10n': 0.0.16 + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + + '@volar/language-service@2.2.5': + dependencies: + '@volar/language-core': 2.2.5 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + + '@volar/snapshot-document@2.2.5': + dependencies: + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.11 + + '@volar/source-map@2.2.5': + dependencies: + muggle-string: 0.4.1 + + '@volar/typescript@2.2.5': + dependencies: + '@volar/language-core': 2.2.5 + path-browserify: 1.0.1 + + '@vscode/emmet-helper@2.9.3': + dependencies: + emmet: 2.4.7 + jsonc-parser: 2.3.1 + vscode-languageserver-textdocument: 1.0.11 + vscode-languageserver-types: 3.17.5 + vscode-uri: 2.1.2 + + '@vscode/l10n@0.0.16': {} + + '@vscode/l10n@0.0.18': {} + + acorn@8.12.0: {} + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + array-iterate@2.0.1: {} + + astro@4.11.0(@types/node@20.14.7)(typescript@5.5.2): + dependencies: + '@astrojs/compiler': 2.8.0 + '@astrojs/internal-helpers': 0.4.0 + '@astrojs/markdown-remark': 5.1.0 + '@astrojs/telemetry': 3.1.0 + '@babel/core': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.7) + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + '@types/babel__core': 7.20.5 + '@types/cookie': 0.6.0 + acorn: 8.12.0 + aria-query: 5.3.0 + axobject-query: 4.0.0 + boxen: 7.1.1 + chokidar: 3.6.0 + ci-info: 4.0.0 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 0.6.0 + cssesc: 3.0.0 + debug: 4.3.5 + deterministic-object-hash: 2.0.2 + devalue: 5.0.0 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.3 + es-module-lexer: 1.5.3 + esbuild: 0.21.5 + estree-walker: 3.0.3 + execa: 8.0.1 + fast-glob: 3.3.2 + flattie: 1.1.1 + github-slugger: 2.0.0 + gray-matter: 4.0.3 + html-escaper: 3.0.3 + http-cache-semantics: 4.1.1 + js-yaml: 4.1.0 + kleur: 4.1.5 + magic-string: 0.30.10 + mrmime: 2.0.0 + ora: 8.0.1 + p-limit: 5.0.0 + p-queue: 8.0.1 + path-to-regexp: 6.2.2 + preferred-pm: 3.1.3 + prompts: 2.4.2 + rehype: 13.0.1 + resolve: 1.22.8 + semver: 7.6.2 + shiki: 1.9.0 + string-width: 7.1.0 + strip-ansi: 7.1.0 + tsconfck: 3.1.0(typescript@5.5.2) + unist-util-visit: 5.0.0 + vfile: 6.0.1 + vite: 5.3.1(@types/node@20.14.7) + vitefu: 0.2.5(vite@5.3.1(@types/node@20.14.7)) + which-pm: 2.2.0 + yargs-parser: 21.1.1 + zod: 3.23.8 + zod-to-json-schema: 3.23.1(zod@3.23.8) + optionalDependencies: + sharp: 0.33.4 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + - typescript + + axobject-query@4.0.0: + dependencies: + dequal: 2.0.3 + + bail@2.0.2: {} + + base-64@1.0.0: {} + + binary-extensions@2.3.0: {} + + boxen@7.1.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.3.0 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.23.1: + dependencies: + caniuse-lite: 1.0.30001636 + electron-to-chromium: 1.4.808 + node-releases: 2.0.14 + update-browserslist-db: 1.0.16(browserslist@4.23.1) + + camelcase@7.0.1: {} + + caniuse-lite@1.0.30001636: {} + + ccount@2.0.1: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@5.3.0: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + ci-info@4.0.0: {} + + cli-boxes@3.0.0: {} + + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + + cli-spinners@2.9.2: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + comma-separated-tokens@2.0.3: {} + + common-ancestor-path@1.0.1: {} + + convert-source-map@2.0.0: {} + + cookie@0.6.0: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + debug@4.3.5: + dependencies: + ms: 2.1.2 + + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + + detect-libc@2.0.3: + optional: true + + deterministic-object-hash@2.0.2: + dependencies: + base-64: 1.0.0 + + devalue@5.0.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@5.2.0: {} + + dlv@1.1.3: {} + + dset@3.1.3: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.4.808: {} + + emmet@2.4.7: + dependencies: + '@emmetio/abbreviation': 2.3.3 + '@emmetio/css-abbreviation': 2.1.8 + + emoji-regex@10.3.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@4.5.0: {} + + es-module-lexer@1.5.3: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.1.2: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@5.0.0: {} + + esprima@4.0.1: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.5 + + eventemitter3@5.0.1: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-yarn-workspace-root2@1.2.16: + dependencies: + micromatch: 4.0.7 + pkg-dir: 4.2.0 + + flattie@1.1.1: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.2.0: {} + + get-stream@8.0.1: {} + + github-slugger@2.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globals@11.12.0: {} + + graceful-fs@4.2.11: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + has-flag@3.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-from-html@2.0.1: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.1 + parse5: 7.1.2 + vfile: 6.0.1 + vfile-message: 4.0.2 + + hast-util-from-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + devlop: 1.1.0 + hastscript: 8.0.0 + property-information: 6.5.0 + vfile: 6.0.1 + vfile-location: 5.0.2 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.0.4: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + '@ungap/structured-clone': 1.2.0 + hast-util-from-parse5: 8.0.1 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.1.2 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-html@9.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-raw: 9.0.4 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.1.1: {} + + human-signals@5.0.0: {} + + import-meta-resolve@4.1.0: {} + + is-arrayish@0.3.2: + optional: true + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-buffer@2.0.5: {} + + is-core-module@2.14.0: + dependencies: + hasown: 2.0.2 + + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-stream@3.0.0: {} + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.0.0: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@2.5.2: {} + + json5@2.2.3: {} + + jsonc-parser@2.3.1: {} + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + load-yaml-file@0.2.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + log-symbols@6.0.0: + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + + longest-streak@3.1.0: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.10: + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + + markdown-table@3.0.3: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.2 + unist-util-visit: 5.0.0 + + mdast-util-find-and-replace@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.2 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.1.0 + + mdast-util-gfm-footnote@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.3 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.1 + mdast-util-gfm-autolink-literal: 2.0.0 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + + mdast-util-to-markdown@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.2 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-decode-string: 2.0.0 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.0 + micromark-factory-label: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-factory-title: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-html-tag-name: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-autolink-literal@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-footnote@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-strikethrough@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-table@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-extension-gfm-task-list-item@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.0.0 + micromark-extension-gfm-footnote: 2.0.0 + micromark-extension-gfm-strikethrough: 2.0.0 + micromark-extension-gfm-table: 2.0.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.0.1 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-destination@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-label@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-space@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-types: 2.0.0 + + micromark-factory-title@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-whitespace@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-character@2.1.0: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-chunked@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-classify-character@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-combine-extensions@2.0.0: + dependencies: + micromark-util-chunked: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-decode-numeric-character-reference@2.0.1: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-decode-string@2.0.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-symbol: 2.0.0 + + micromark-util-encode@2.0.0: {} + + micromark-util-html-tag-name@2.0.0: {} + + micromark-util-normalize-identifier@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-resolve-all@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-util-sanitize-uri@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + micromark-util-subtokenize@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-symbol@2.0.0: {} + + micromark-util-types@2.0.0: {} + + micromark@4.0.0: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.5 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mrmime@2.0.0: {} + + ms@2.1.2: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.7: {} + + nlcst-to-string@3.1.1: + dependencies: + '@types/nlcst': 1.0.4 + + node-releases@2.0.14: {} + + normalize-path@3.0.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + obuf@1.1.2: {} + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + ora@8.0.1: + dependencies: + chalk: 5.3.0 + cli-cursor: 4.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.0.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.1.0 + strip-ansi: 7.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@5.0.0: + dependencies: + yocto-queue: 1.0.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-queue@8.0.1: + dependencies: + eventemitter3: 5.0.1 + p-timeout: 6.1.2 + + p-timeout@6.1.2: {} + + p-try@2.2.0: {} + + parse-latin@5.0.1: + dependencies: + nlcst-to-string: 3.1.1 + unist-util-modify-children: 3.1.1 + unist-util-visit-children: 2.0.2 + + parse5@7.1.2: + dependencies: + entities: 4.5.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-to-regexp@6.2.2: {} + + pg-cloudflare@1.1.1: + optional: true + + pg-connection-string@2.6.4: {} + + pg-int8@1.0.1: {} + + pg-numeric@1.0.2: {} + + pg-pool@3.6.2(pg@8.12.0): + dependencies: + pg: 8.12.0 + + pg-protocol@1.6.1: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg-types@4.0.2: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + + pg@8.12.0: + dependencies: + pg-connection-string: 2.6.4 + pg-pool: 3.6.2(pg@8.12.0) + pg-protocol: 1.6.1 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.0.1: {} + + picomatch@2.3.1: {} + + pify@4.0.1: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + postcss@8.4.38: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + + postgres-array@2.0.0: {} + + postgres-array@3.0.2: {} + + postgres-bytea@1.0.0: {} + + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + + postgres-date@1.0.7: {} + + postgres-date@2.1.0: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} + + preferred-pm@3.1.3: + dependencies: + find-up: 5.0.0 + find-yarn-workspace-root2: 1.2.16 + path-exists: 4.0.0 + which-pm: 2.0.0 + + prismjs@1.29.0: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + queue-microtask@1.2.3: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + rehype-parse@9.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.1 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.0.4 + vfile: 6.0.1 + + rehype-stringify@10.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.1 + unified: 11.0.5 + + rehype@13.0.1: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.0 + rehype-stringify: 10.0.0 + unified: 11.0.5 + + remark-gfm@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.1 + micromark-util-types: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.1 + + remark-smartypants@2.1.0: + dependencies: + retext: 8.1.0 + retext-smartypants: 5.2.0 + unist-util-visit: 5.0.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.0 + unified: 11.0.5 + + request-light@0.7.0: {} + + require-directory@2.1.1: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.14.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retext-latin@3.1.0: + dependencies: + '@types/nlcst': 1.0.4 + parse-latin: 5.0.1 + unherit: 3.0.1 + unified: 10.1.2 + + retext-smartypants@5.2.0: + dependencies: + '@types/nlcst': 1.0.4 + nlcst-to-string: 3.1.1 + unified: 10.1.2 + unist-util-visit: 4.1.2 + + retext-stringify@3.1.0: + dependencies: + '@types/nlcst': 1.0.4 + nlcst-to-string: 3.1.1 + unified: 10.1.2 + + retext@8.1.0: + dependencies: + '@types/nlcst': 1.0.4 + retext-latin: 3.1.0 + retext-stringify: 3.1.0 + unified: 10.1.2 + + reusify@1.0.4: {} + + rollup@4.18.0: + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.18.0 + '@rollup/rollup-android-arm64': 4.18.0 + '@rollup/rollup-darwin-arm64': 4.18.0 + '@rollup/rollup-darwin-x64': 4.18.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.18.0 + '@rollup/rollup-linux-arm-musleabihf': 4.18.0 + '@rollup/rollup-linux-arm64-gnu': 4.18.0 + '@rollup/rollup-linux-arm64-musl': 4.18.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.18.0 + '@rollup/rollup-linux-riscv64-gnu': 4.18.0 + '@rollup/rollup-linux-s390x-gnu': 4.18.0 + '@rollup/rollup-linux-x64-gnu': 4.18.0 + '@rollup/rollup-linux-x64-musl': 4.18.0 + '@rollup/rollup-win32-arm64-msvc': 4.18.0 + '@rollup/rollup-win32-ia32-msvc': 4.18.0 + '@rollup/rollup-win32-x64-msvc': 4.18.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + semver@6.3.1: {} + + semver@7.6.2: {} + + sharp@0.33.4: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.4 + '@img/sharp-darwin-x64': 0.33.4 + '@img/sharp-libvips-darwin-arm64': 1.0.2 + '@img/sharp-libvips-darwin-x64': 1.0.2 + '@img/sharp-libvips-linux-arm': 1.0.2 + '@img/sharp-libvips-linux-arm64': 1.0.2 + '@img/sharp-libvips-linux-s390x': 1.0.2 + '@img/sharp-libvips-linux-x64': 1.0.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + '@img/sharp-linux-arm': 0.33.4 + '@img/sharp-linux-arm64': 0.33.4 + '@img/sharp-linux-s390x': 0.33.4 + '@img/sharp-linux-x64': 0.33.4 + '@img/sharp-linuxmusl-arm64': 0.33.4 + '@img/sharp-linuxmusl-x64': 0.33.4 + '@img/sharp-wasm32': 0.33.4 + '@img/sharp-win32-ia32': 0.33.4 + '@img/sharp-win32-x64': 0.33.4 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shiki@1.9.0: + dependencies: + '@shikijs/core': 1.9.0 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + sisteransi@1.0.5: {} + + source-map-js@1.2.0: {} + + space-separated-tokens@2.0.2: {} + + split2@4.2.0: {} + + sprintf-js@1.0.3: {} + + stdin-discarder@0.2.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.1.0: + dependencies: + emoji-regex: 10.3.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + + strip-bom-string@1.0.0: {} + + strip-bom@3.0.0: {} + + strip-final-newline@3.0.0: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tsconfck@3.1.0(typescript@5.5.2): + optionalDependencies: + typescript: 5.5.2 + + tslib@2.6.3: + optional: true + + type-fest@2.19.0: {} + + typesafe-path@0.2.2: {} + + typescript-auto-import-cache@0.3.3: + dependencies: + semver: 7.6.2 + + typescript@5.5.2: {} + + undici-types@5.26.5: {} + + unherit@3.0.1: {} + + unified@10.1.2: + dependencies: + '@types/unist': 2.0.10 + bail: 2.0.2 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 5.3.7 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.2 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.1 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + + unist-util-is@5.2.1: + dependencies: + '@types/unist': 2.0.10 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.2 + + unist-util-modify-children@3.1.1: + dependencies: + '@types/unist': 2.0.10 + array-iterate: 2.0.1 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.2 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.2 + unist-util-visit: 5.0.0 + + unist-util-stringify-position@3.0.3: + dependencies: + '@types/unist': 2.0.10 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.2 + + unist-util-visit-children@2.0.2: + dependencies: + '@types/unist': 2.0.10 + + unist-util-visit-parents@5.1.3: + dependencies: + '@types/unist': 2.0.10 + unist-util-is: 5.2.1 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + + unist-util-visit@4.1.2: + dependencies: + '@types/unist': 2.0.10 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + update-browserslist-db@1.0.16(browserslist@4.23.1): + dependencies: + browserslist: 4.23.1 + escalade: 3.1.2 + picocolors: 1.0.1 + + vfile-location@5.0.2: + dependencies: + '@types/unist': 3.0.2 + vfile: 6.0.1 + + vfile-message@3.1.4: + dependencies: + '@types/unist': 2.0.10 + unist-util-stringify-position: 3.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + + vfile@5.3.7: + dependencies: + '@types/unist': 2.0.10 + is-buffer: 2.0.5 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + + vfile@6.0.1: + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + + vite@5.3.1(@types/node@20.14.7): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.38 + rollup: 4.18.0 + optionalDependencies: + '@types/node': 20.14.7 + fsevents: 2.3.3 + + vitefu@0.2.5(vite@5.3.1(@types/node@20.14.7)): + optionalDependencies: + vite: 5.3.1(@types/node@20.14.7) + + volar-service-css@0.0.45(@volar/language-service@2.2.5): + dependencies: + vscode-css-languageservice: 6.2.14 + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.2.5 + + volar-service-emmet@0.0.45(@volar/language-service@2.2.5): + dependencies: + '@emmetio/css-parser': 0.4.0 + '@emmetio/html-matcher': 1.3.0 + '@vscode/emmet-helper': 2.9.3 + optionalDependencies: + '@volar/language-service': 2.2.5 + + volar-service-html@0.0.45(@volar/language-service@2.2.5): + dependencies: + vscode-html-languageservice: '@johnsoncodehk/vscode-html-languageservice@5.2.0-34a5462' + vscode-languageserver-textdocument: 1.0.11 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.2.5 + + volar-service-prettier@0.0.45(@volar/language-service@2.2.5): + dependencies: + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.2.5 + + volar-service-typescript-twoslash-queries@0.0.45(@volar/language-service@2.2.5): + optionalDependencies: + '@volar/language-service': 2.2.5 + + volar-service-typescript@0.0.45(@volar/language-service@2.2.5): + dependencies: + path-browserify: 1.0.1 + semver: 7.6.2 + typescript-auto-import-cache: 0.3.3 + vscode-languageserver-textdocument: 1.0.11 + vscode-nls: 5.2.0 + optionalDependencies: + '@volar/language-service': 2.2.5 + + vscode-css-languageservice@6.2.14: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.11 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + vscode-html-languageservice@5.2.0: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.11 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.11: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-nls@5.2.0: {} + + vscode-uri@2.1.2: {} + + vscode-uri@3.0.8: {} + + web-namespaces@2.0.1: {} + + which-pm-runs@1.1.0: {} + + which-pm@2.0.0: + dependencies: + load-yaml-file: 0.2.0 + path-exists: 4.0.0 + + which-pm@2.2.0: + dependencies: + load-yaml-file: 0.2.0 + path-exists: 4.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@4.0.1: + dependencies: + string-width: 5.1.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.0.0: {} + + zod-to-json-schema@3.23.1(zod@3.23.8): + dependencies: + zod: 3.23.8 + + zod@3.23.8: {} + + zwitch@2.0.4: {} diff --git a/indexer/postgres/graphql-proxy/public/favicon.svg b/indexer/postgres/graphql-proxy/public/favicon.svg new file mode 100644 index 000000000000..f157bd1c5e28 --- /dev/null +++ b/indexer/postgres/graphql-proxy/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/indexer/postgres/graphql-proxy/src/env.d.ts b/indexer/postgres/graphql-proxy/src/env.d.ts new file mode 100644 index 000000000000..f964fe0cffd8 --- /dev/null +++ b/indexer/postgres/graphql-proxy/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/indexer/postgres/graphql-proxy/src/pages/graphiql.astro b/indexer/postgres/graphql-proxy/src/pages/graphiql.astro new file mode 100644 index 000000000000..02674b666423 --- /dev/null +++ b/indexer/postgres/graphql-proxy/src/pages/graphiql.astro @@ -0,0 +1,81 @@ + + + + + GraphiQL + + + + + + + + + + + + + + +
Loading...
+ + + \ No newline at end of file diff --git a/indexer/postgres/graphql-proxy/src/pages/graphql.ts b/indexer/postgres/graphql-proxy/src/pages/graphql.ts new file mode 100644 index 000000000000..e9791859d4ef --- /dev/null +++ b/indexer/postgres/graphql-proxy/src/pages/graphql.ts @@ -0,0 +1,47 @@ +import pg from 'pg' + +const {Client} = pg +const client = new Client({ + connectionString: import.meta.env.DATABASE_URL, +}) +await client.connect() + + +export async function GET({params, request}) { + const {query, variables, operationName} = params; + + return graphql(query, variables, operationName); +} + +export async function POST({params, request}) { + const {query, operationName, variables} = await request.json(); + return graphql(query, variables, operationName); +} + +async function graphql(query, variables, operationName) { + try { + const res = await client.query( + 'select graphql.resolve($1, $2, $3);', + [query, variables, operationName] + ); + + return new Response( + JSON.stringify(res.rows[0].resolve), + { + headers: { + 'Content-Type': 'application/json', + } + } + ) + } catch (e) { + return new Response( + JSON.stringify({errors: [e.message]}), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + } + } + ) + } +} diff --git a/indexer/postgres/graphql-proxy/src/pages/index.astro b/indexer/postgres/graphql-proxy/src/pages/index.astro new file mode 100644 index 000000000000..240be0a56aca --- /dev/null +++ b/indexer/postgres/graphql-proxy/src/pages/index.astro @@ -0,0 +1,11 @@ + + + + + + + + + OK + + diff --git a/indexer/postgres/graphql-proxy/tsconfig.json b/indexer/postgres/graphql-proxy/tsconfig.json new file mode 100644 index 000000000000..77da9dd00982 --- /dev/null +++ b/indexer/postgres/graphql-proxy/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/strict" +} \ No newline at end of file diff --git a/indexer/postgres/graphql.go b/indexer/postgres/graphql.go new file mode 100644 index 000000000000..c353c19f137d --- /dev/null +++ b/indexer/postgres/graphql.go @@ -0,0 +1,97 @@ +package postgres + +import ( + "database/sql" + "encoding/json" + "net/http" +) + +type graphqlHandler struct { + conn *sql.DB +} + +func NewGraphQLHandler(conn *sql.DB) http.Handler { + return &graphqlHandler{conn: conn} +} + +var _ http.Handler = &graphqlHandler{} + +type graphqlRequest struct { + Query string `json:"query"` + OperationName string `json:"operationName"` + Variables string `json:"variables"` +} + +type graphqlResponse struct { + Data json.RawMessage `json:"data"` + Errors []error `json:"errors,omitempty"` +} + +func (g graphqlHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + switch request.Method { + case http.MethodGet: + g.handleGet(writer, request) + case http.MethodPost: + g.handlePost(writer, request) + default: + http.Error(writer, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func (g graphqlHandler) handlePost(writer http.ResponseWriter, request *http.Request) { + var req graphqlRequest + err := json.NewDecoder(request.Body).Decode(&req) + if err != nil { + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + + g.handle(writer, request, &req) +} + +func (g graphqlHandler) handleGet(writer http.ResponseWriter, request *http.Request) { + var gqlReq graphqlRequest + gqlReq.Query = request.URL.Query().Get("query") + gqlReq.OperationName = request.URL.Query().Get("operationName") + gqlReq.Variables = request.URL.Query().Get("variables") + + g.handle(writer, request, &gqlReq) +} + +func (g graphqlHandler) handle(writer http.ResponseWriter, request *http.Request, gqlReq *graphqlRequest) { + rows, err := g.conn.QueryContext( + request.Context(), + `select graphql.resolve($1, $2, $3);`, + gqlReq.Query, + gqlReq.OperationName, + gqlReq.Variables, + ) + + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + + } + }(rows) + + var data json.RawMessage + for rows.Next() { + err = rows.Scan(&data) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + } + + resp := graphqlResponse{Data: data} + err = json.NewEncoder(writer).Encode(resp) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + } + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) +} diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index 855ceac10697..4d711e280257 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -3,89 +3,162 @@ package postgres import ( "context" "database/sql" + "encoding/json" "fmt" - indexerbase "cosmossdk.io/indexer/base" + "cosmossdk.io/schema/appdata" + "cosmossdk.io/schema/indexing" + "cosmossdk.io/schema/log" ) type Indexer struct { ctx context.Context db *sql.DB tx *sql.Tx - modules map[string]*moduleManager -} - -type moduleManager struct { - moduleName string - schema indexerbase.ModuleSchema - tables map[string]*TableManager -} + options Options -type Options struct { - Driver string - ConnectionURL string + modules map[string]*ModuleManager } -func NewIndexer(ctx context.Context, opts Options) (*Indexer, error) { - if opts.Driver == "" { - opts.Driver = "pgx" - } - - if opts.ConnectionURL == "" { - return nil, fmt.Errorf("connection URL not set") - } +func (i *Indexer) Initialize(ctx context.Context, data indexing.InitializationData) (indexing.InitializationResult, error) { + i.options.Logger.Info("Starting Postgres Indexer") - db, err := sql.Open(opts.Driver, opts.ConnectionURL) - if err != nil { - return nil, fmt.Errorf("failed to open database: %w", err) - } go func() { <-ctx.Done() - err := db.Close() + err := i.db.Close() if err != nil { panic(fmt.Sprintf("failed to close database: %v", err)) } }() - tx, err := db.BeginTx(ctx, nil) + i.ctx = ctx + + tx, err := i.db.BeginTx(ctx, nil) if err != nil { - return nil, fmt.Errorf("failed to start transaction: %w", err) + return indexing.InitializationResult{}, fmt.Errorf("failed to start transaction: %w", err) } + i.tx = tx + + return indexing.InitializationResult{ + Listener: i.listener(), + }, nil +} + +type configOptions struct { + DatabaseDriver string `json:"database_driver"` + DatabaseURL string `json:"database_url"` + RetainDeletions bool `json:"retain_deletions"` +} + +func init() { + indexing.RegisterIndexer("postgres", func(rawOpts map[string]interface{}, resources indexing.IndexerResources) (indexing.Indexer, error) { + bz, err := json.Marshal(rawOpts) + if err != nil { + return nil, fmt.Errorf("failed to marshal options: %w", err) + } + + var opts configOptions + err = json.Unmarshal(bz, &opts) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal options: %w", err) + } + + if opts.DatabaseDriver == "" { + opts.DatabaseDriver = "pgx" + } + + if opts.DatabaseURL == "" { + return nil, fmt.Errorf("connection URL not set") + } + + db, err := sql.Open(opts.DatabaseDriver, opts.DatabaseURL) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + return NewIndexer(db, Options{ + RetainDeletions: opts.RetainDeletions, + Logger: resources.Logger, + }) + }) +} + +type Options struct { + RetainDeletions bool + Logger log.Logger +} + +func NewIndexer(db *sql.DB, opts Options) (*Indexer, error) { return &Indexer{ - ctx: ctx, db: db, - tx: tx, - modules: map[string]*moduleManager{}, + modules: map[string]*ModuleManager{}, + options: opts, }, nil } -func (i *Indexer) Listener() indexerbase.Listener { - return indexerbase.Listener{ - InitializeModuleSchema: i.initModuleSchema, +func (i *Indexer) listener() appdata.Listener { + return appdata.Listener{ + InitializeModuleData: i.initModuleSchema, + OnObjectUpdate: i.onObjectUpdate, + Commit: i.commit, } } -func (i *Indexer) initModuleSchema(moduleName string, schema indexerbase.ModuleSchema) error { +func (i *Indexer) initModuleSchema(data appdata.ModuleInitializationData) error { + moduleName := data.ModuleName + modSchema := data.Schema _, ok := i.modules[moduleName] if ok { return fmt.Errorf("module %s already initialized", moduleName) } - mm := &moduleManager{ - moduleName: moduleName, - schema: schema, - tables: map[string]*TableManager{}, + mm := newModuleManager(moduleName, modSchema, i.options) + i.modules[moduleName] = mm + + return mm.Init(i.ctx, i.tx) +} + +func (i *Indexer) onObjectUpdate(data appdata.ObjectUpdateData) error { + module := data.ModuleName + mod, ok := i.modules[module] + if !ok { + return fmt.Errorf("module %s not initialized", module) } - for _, typ := range schema.ObjectTypes { - tm := NewTableManager(moduleName, typ) - mm.tables[typ.Name] = tm - err := tm.CreateTable(i.ctx, i.tx) + for _, update := range data.Updates { + tm, ok := mod.Tables[update.TypeName] + if !ok { + return fmt.Errorf("object type %s not found in schema for module %s", update.TypeName, module) + } + + var err error + if update.Delete { + err = tm.Delete(i.ctx, i.tx, update.Key) + } else { + err = tm.InsertUpdate(i.ctx, i.tx, update.Key, update.Value) + } if err != nil { - return fmt.Errorf("failed to create table for %s in module %s: %w", typ.Name, moduleName, err) + return err } } - return nil } + +func (i *Indexer) commit(_ appdata.CommitData) error { + err := i.tx.Commit() + if err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + + i.tx, err = i.db.BeginTx(i.ctx, nil) + return err +} + +func (i *Indexer) ActiveTx() *sql.Tx { + return i.tx +} + +func (i *Indexer) Modules() map[string]*ModuleManager { + return i.modules +} diff --git a/indexer/postgres/insert_update.go b/indexer/postgres/insert_update.go new file mode 100644 index 000000000000..af651da7ad5a --- /dev/null +++ b/indexer/postgres/insert_update.go @@ -0,0 +1,101 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "io" + "strings" +) + +func (tm *TableManager) InsertUpdate(ctx context.Context, tx *sql.Tx, key, value interface{}) error { + exists, err := tm.Exists(ctx, tx, key) + if err != nil { + return err + } + + buf := new(strings.Builder) + var params []interface{} + if exists { + params, err = tm.UpdateSql(buf, key, value) + } else { + params, err = tm.InsertSql(buf, key, value) + } + + sqlStr := buf.String() + tm.options.Logger.Debug("Insert or Update", "sql", sqlStr, "params", params) + _, err = tx.ExecContext(ctx, sqlStr, params...) + return err +} + +func (tm *TableManager) InsertSql(w io.Writer, key, value interface{}) ([]interface{}, error) { + keyParams, keyCols, err := tm.bindKeyParams(key) + if err != nil { + return nil, err + } + + valueParams, valueCols, err := tm.bindValueParams(value) + if err != nil { + return nil, err + } + + var allParams []interface{} + allParams = append(allParams, keyParams...) + allParams = append(allParams, valueParams...) + + allCols := make([]string, 0, len(keyCols)+len(valueCols)) + allCols = append(allCols, keyCols...) + allCols = append(allCols, valueCols...) + + var paramBindings []string + for i := 1; i <= len(allCols); i++ { + paramBindings = append(paramBindings, fmt.Sprintf("$%d", i)) + } + + _, err = fmt.Fprintf(w, "INSERT INTO %q (%s) VALUES (%s);", tm.TableName(), + strings.Join(allCols, ", "), + strings.Join(paramBindings, ", "), + ) + return allParams, err +} + +func (tm *TableManager) UpdateSql(w io.Writer, key, value interface{}) ([]interface{}, error) { + _, err := fmt.Fprintf(w, "UPDATE %q SET ", tm.TableName()) + + valueParams, valueCols, err := tm.bindValueParams(value) + if err != nil { + return nil, err + } + + paramIdx := 1 + for i, col := range valueCols { + if i > 0 { + _, err = fmt.Fprintf(w, ", ") + if err != nil { + return nil, err + } + } + _, err = fmt.Fprintf(w, "%s = $%d", col, paramIdx) + if err != nil { + return nil, err + } + + paramIdx++ + } + + if tm.options.RetainDeletions && tm.typ.RetainDeletions { + _, err = fmt.Fprintf(w, ", _deleted = FALSE") + if err != nil { + return nil, err + } + } + + _, keyParams, err := tm.WhereSqlAndParams(w, key, paramIdx) + if err != nil { + return nil, err + } + + allParams := append(valueParams, keyParams...) + _, err = fmt.Fprintf(w, ";") + return allParams, err +} diff --git a/indexer/postgres/module_mgr.go b/indexer/postgres/module_mgr.go new file mode 100644 index 000000000000..8d6c7f403db8 --- /dev/null +++ b/indexer/postgres/module_mgr.go @@ -0,0 +1,57 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + + "cosmossdk.io/schema" +) + +type ModuleManager struct { + moduleName string + schema schema.ModuleSchema + // TODO: make private or internal + Tables map[string]*TableManager + definedEnums map[string]schema.EnumDefinition + options Options +} + +func newModuleManager(moduleName string, modSchema schema.ModuleSchema, options Options) *ModuleManager { + return &ModuleManager{ + moduleName: moduleName, + schema: modSchema, + Tables: map[string]*TableManager{}, + definedEnums: map[string]schema.EnumDefinition{}, + options: options, + } +} + +func (m *ModuleManager) Init(ctx context.Context, tx *sql.Tx) error { + // create enum types + for _, typ := range m.schema.ObjectTypes { + err := m.createEnumTypesForFields(ctx, tx, typ.KeyFields) + if err != nil { + return err + } + + err = m.createEnumTypesForFields(ctx, tx, typ.ValueFields) + if err != nil { + return err + } + } + + // create tables for all object types + // NOTE: if we want to support foreign keys, we need to sort tables ind dependency order + for _, typ := range m.schema.ObjectTypes { + tm := NewTableManager(m.moduleName, typ, m.options) + m.Tables[typ.Name] = tm + err := tm.CreateTable(ctx, tx) + if err != nil { + return fmt.Errorf("failed to create table for %s in module %s: %w", typ.Name, m.moduleName, err) + } + } + + return nil + +} diff --git a/indexer/postgres/params.go b/indexer/postgres/params.go new file mode 100644 index 000000000000..1d9d04072161 --- /dev/null +++ b/indexer/postgres/params.go @@ -0,0 +1,117 @@ +package postgres + +import ( + "fmt" + "time" + + "github.com/cosmos/btcutil/bech32" + + "cosmossdk.io/schema" +) + +func (tm *TableManager) bindKeyParams(key interface{}) ([]interface{}, []string, error) { + n := len(tm.typ.KeyFields) + if n == 0 { + // singleton, set _id = 1 + return []interface{}{1}, []string{"_id"}, nil + } else if n == 1 { + return tm.bindParams(tm.typ.KeyFields, []interface{}{key}) + } else { + key, ok := key.([]interface{}) + if !ok { + return nil, nil, fmt.Errorf("expected key to be a slice") + } + + return tm.bindParams(tm.typ.KeyFields, key) + } +} + +func (tm *TableManager) bindValueParams(value interface{}) (params []interface{}, valueCols []string, err error) { + n := len(tm.typ.ValueFields) + if n == 0 { + return nil, nil, nil + } else if valueUpdates, ok := value.(schema.ValueUpdates); ok { + var e error + var fields []schema.Field + var params []interface{} + if err := valueUpdates.Iterate(func(name string, value interface{}) bool { + field, ok := tm.valueFields[name] + if !ok { + e = fmt.Errorf("unknown column %q", name) + return false + } + fields = append(fields, field) + params = append(params, value) + return true + }); err != nil { + return nil, nil, err + } + if e != nil { + return nil, nil, e + } + + return tm.bindParams(fields, params) + } else if n == 1 { + return tm.bindParams(tm.typ.ValueFields, []interface{}{value}) + } else { + values, ok := value.([]interface{}) + if !ok { + return nil, nil, fmt.Errorf("expected values to be a slice") + } + + return tm.bindParams(tm.typ.ValueFields, values) + } +} + +func (tm *TableManager) bindParams(fields []schema.Field, values []interface{}) ([]interface{}, []string, error) { + names := make([]string, 0, len(fields)) + params := make([]interface{}, 0, len(fields)) + for i, field := range fields { + if i >= len(values) { + return nil, nil, fmt.Errorf("missing value for field %q", field.Name) + } + + param, err := tm.bindParam(field, values[i]) + if err != nil { + return nil, nil, err + } + + name, err := tm.updatableColumnName(field) + if err != nil { + return nil, nil, err + } + + names = append(names, name) + params = append(params, param) + } + return params, names, nil +} + +func (tm *TableManager) bindParam(field schema.Field, value interface{}) (param interface{}, err error) { + param = value + if value == nil { + if !field.Nullable { + return nil, fmt.Errorf("expected non-null value for field %q", field.Name) + } + } else if field.Kind == schema.TimeKind { + t, ok := value.(time.Time) + if !ok { + return nil, fmt.Errorf("expected time.Time value for field %q, got %T", field.Name, value) + } + + param = t.UnixNano() + } else if field.Kind == schema.DurationKind { + t, ok := value.(time.Duration) + if !ok { + return nil, fmt.Errorf("expected time.Duration value for field %q, got %T", field.Name, value) + } + + param = int64(t) + } else if field.Kind == schema.Bech32AddressKind { + param, err = bech32.EncodeFromBase256(field.AddressPrefix, value.([]byte)) + if err != nil { + return nil, fmt.Errorf("encoding bech32 failed: %w", err) + } + } + return +} diff --git a/indexer/postgres/select.go b/indexer/postgres/select.go new file mode 100644 index 000000000000..5325353f9942 --- /dev/null +++ b/indexer/postgres/select.go @@ -0,0 +1,91 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "io" + "strings" +) + +func (tm *TableManager) Exists(ctx context.Context, tx *sql.Tx, key interface{}) (bool, error) { + buf := new(strings.Builder) + params, err := tm.ExistsSqlAndParams(buf, key) + if err != nil { + return false, err + } + + return tm.checkExists(ctx, tx, buf.String(), params) +} + +func (tm *TableManager) ExistsSqlAndParams(w io.Writer, key interface{}) ([]interface{}, error) { + _, err := fmt.Fprintf(w, "SELECT 1 FROM %q", tm.TableName()) + if err != nil { + return nil, err + } + + _, keyParams, err := tm.WhereSqlAndParams(w, key, 1) + if err != nil { + return nil, err + } + + _, err = fmt.Fprintf(w, ";") + return keyParams, err +} + +func (tm *TableManager) Equals(ctx context.Context, tx *sql.Tx, key, val interface{}) (bool, error) { + buf := new(strings.Builder) + params, err := tm.EqualsSqlAndParams(buf, key, val) + if err != nil { + return false, err + } + + return tm.checkExists(ctx, tx, buf.String(), params) +} + +func (tm *TableManager) EqualsSqlAndParams(w io.Writer, key, val interface{}) ([]interface{}, error) { + _, err := fmt.Fprintf(w, "SELECT 1 FROM %q", tm.TableName()) + if err != nil { + return nil, err + } + + keyParams, keyCols, err := tm.bindKeyParams(key) + if err != nil { + return nil, err + } + + valueParams, valueCols, err := tm.bindValueParams(val) + if err != nil { + return nil, err + } + + allParams := make([]interface{}, 0, len(keyParams)+len(valueParams)) + allParams = append(allParams, keyParams...) + allParams = append(allParams, valueParams...) + + allCols := make([]string, 0, len(keyCols)+len(valueCols)) + allCols = append(allCols, keyCols...) + allCols = append(allCols, valueCols...) + + _, allParams, err = tm.WhereSql(w, allParams, allCols, 1) + if err != nil { + return nil, err + } + + _, err = fmt.Fprintf(w, ";") + return allParams, err +} + +func (tm *TableManager) checkExists(ctx context.Context, tx *sql.Tx, sqlStr string, params []interface{}) (bool, error) { + tm.options.Logger.Debug("Select", "sql", sqlStr, "params", params) + var res interface{} + err := tx.QueryRowContext(ctx, sqlStr, params...).Scan(&res) + switch err { + case nil: + return true, nil + case sql.ErrNoRows: + return false, nil + default: + return false, err + } +} diff --git a/indexer/postgres/table_mgr.go b/indexer/postgres/table_mgr.go index fbe09dced5ab..9210230bcc41 100644 --- a/indexer/postgres/table_mgr.go +++ b/indexer/postgres/table_mgr.go @@ -3,23 +3,41 @@ package postgres import ( "fmt" - indexerbase "cosmossdk.io/indexer/base" + "cosmossdk.io/schema" ) // TableManager is a helper struct that generates SQL for a given object type. type TableManager struct { - moduleName string - typ indexerbase.ObjectType + moduleName string + typ schema.ObjectType + valueFields map[string]schema.Field + allFields map[string]schema.Field + options Options } // NewTableManager creates a new TableManager for the given object type. -func NewTableManager(moduleName string, typ indexerbase.ObjectType) *TableManager { +func NewTableManager(moduleName string, typ schema.ObjectType, options Options) *TableManager { + allFields := make(map[string]schema.Field) + valueFields := make(map[string]schema.Field) + + for _, field := range typ.KeyFields { + allFields[field.Name] = field + } + + for _, field := range typ.ValueFields { + valueFields[field.Name] = field + allFields[field.Name] = field + } + return &TableManager{ - moduleName: moduleName, - typ: typ, + moduleName: moduleName, + typ: typ, + allFields: allFields, + valueFields: valueFields, + options: options, } } -func (t *TableManager) TableName() string { - return fmt.Sprintf("%s_%s", t.moduleName, t.typ.Name) +func (tm *TableManager) TableName() string { + return fmt.Sprintf("%s_%s", tm.moduleName, tm.typ.Name) } diff --git a/indexer/postgres/testing/app/main.go b/indexer/postgres/testing/app/main.go new file mode 100644 index 000000000000..9569c0d53187 --- /dev/null +++ b/indexer/postgres/testing/app/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "database/sql" + "os" + + "cosmossdk.io/log" + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + "github.com/hashicorp/consul/sdk/freeport" + _ "github.com/jackc/pgx/v5/stdlib" + + "cosmossdk.io/indexer/postgres" + "cosmossdk.io/schema/indexing" + indexertesting "cosmossdk.io/schema/testing" + appdatatest "cosmossdk.io/schema/testing/appdata" + "cosmossdk.io/schema/testing/statesim" +) + +func start() error { + dbUrl, found := os.LookupEnv("DATABASE_URL") + if !found { + tempDir, err := os.MkdirTemp("", "postgres-indexer-test") + if err != nil { + return err + } + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + panic(err) + } + }(tempDir) + + dbPort := freeport.MustTake(1)[0] + pgConfig := embeddedpostgres.DefaultConfig(). + Port(uint32(dbPort)). + DataPath(tempDir) + + dbUrl = pgConfig.GetConnectionURL() + pg := embeddedpostgres.NewDatabase(pgConfig) + err = pg.Start() + if err != nil { + return err + } + defer func(pg *embeddedpostgres.EmbeddedPostgres) { + err := pg.Stop() + if err != nil { + panic(err) + } + }(pg) + } + + db, err := sql.Open("pgx", dbUrl) + if err != nil { + return err + } + + indexer, err := postgres.NewIndexer(db, postgres.Options{ + Logger: log.NewLogger(os.Stdout), + }) + if err != nil { + return err + } + + res, err := indexer.Initialize(context.Background(), indexing.InitializationData{}) + if err != nil { + return err + } + + fixture := appdatatest.NewSimulator(appdatatest.SimulatorOptions{ + Listener: res.Listener, + AppSchema: indexertesting.ExampleAppSchema, + StateSimOptions: statesim.Options{}, + }) + + err = fixture.Initialize() + if err != nil { + return err + } + + blockDataGen := fixture.BlockDataGenN(1000) + i := 0 + for { + blockData := blockDataGen.Example(i) + err = fixture.ProcessBlockData(blockData) + if err != nil { + return err + } + i++ + } + + return nil +} + +func main() { + err := start() + if err != nil { + panic(err) + } +} diff --git a/indexer/postgres/testing/go.mod b/indexer/postgres/testing/go.mod index 218dda61adc8..76dbbab8c311 100644 --- a/indexer/postgres/testing/go.mod +++ b/indexer/postgres/testing/go.mod @@ -2,7 +2,9 @@ module cosmossdk.io/indexer/postgres/testing require ( cosmossdk.io/indexer/postgres v0.0.0 - cosmossdk.io/indexer/testing v0.0.0-00010101000000-000000000000 + cosmossdk.io/log v1.3.1 + cosmossdk.io/schema v0.0.0 + cosmossdk.io/schema/testing v0.0.0-00010101000000-000000000000 github.com/fergusstrange/embedded-postgres v1.27.0 github.com/hashicorp/consul/sdk v0.16.1 github.com/jackc/pgx/v5 v5.6.0 @@ -10,28 +12,33 @@ require ( ) require ( - cosmossdk.io/indexer/base v0.0.0 // indirect - github.com/brianvoe/gofakeit/v7 v7.0.3 // indirect + github.com/cosmos/btcutil v1.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.4 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rs/zerolog v1.32.0 // indirect + github.com/tidwall/btree v1.7.0 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + pgregory.net/rapid v1.1.0 // indirect ) -replace cosmossdk.io/indexer/base => ../../base - -replace cosmossdk.io/indexer/testing => ../../testing +replace cosmossdk.io/schema/testing => ../../../schema/testing replace cosmossdk.io/indexer/postgres => .. +replace cosmossdk.io/schema => ../../../schema + go 1.22 diff --git a/indexer/postgres/testing/go.sum b/indexer/postgres/testing/go.sum index 8f3bc81afa81..6f4fc275583d 100644 --- a/indexer/postgres/testing/go.sum +++ b/indexer/postgres/testing/go.sum @@ -1,11 +1,17 @@ -github.com/brianvoe/gofakeit/v7 v7.0.3 h1:tGCt+eYfhTMWE1ko5G2EO1f/yE44yNpIwUb4h32O0wo= -github.com/brianvoe/gofakeit/v7 v7.0.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= +cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= +cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fergusstrange/embedded-postgres v1.27.0 h1:RAlpWL194IhEpPgeJceTM0ifMJKhiSVxBVIDYB1Jee8= github.com/fergusstrange/embedded-postgres v1.27.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -22,16 +28,29 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= @@ -40,6 +59,9 @@ golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -50,3 +72,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV 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= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/indexer/postgres/testing/postgres_test.go b/indexer/postgres/testing/postgres_test.go index ae14efca9604..d7af95cc1d8c 100644 --- a/indexer/postgres/testing/postgres_test.go +++ b/indexer/postgres/testing/postgres_test.go @@ -2,22 +2,44 @@ package testing import ( "context" + "database/sql" + "fmt" + "os" "testing" + "cosmossdk.io/log" embeddedpostgres "github.com/fergusstrange/embedded-postgres" "github.com/hashicorp/consul/sdk/freeport" + _ "github.com/jackc/pgx/v5/stdlib" "github.com/stretchr/testify/require" - indexertesting "cosmossdk.io/indexer/testing" - "cosmossdk.io/indexer/postgres" - - _ "github.com/jackc/pgx/v5/stdlib" + "cosmossdk.io/schema" + "cosmossdk.io/schema/appdata" + "cosmossdk.io/schema/indexing" + indexertesting "cosmossdk.io/schema/testing" + appdatatest "cosmossdk.io/schema/testing/appdata" + "cosmossdk.io/schema/testing/statesim" ) func TestPostgresIndexer(t *testing.T) { + t.Run("RetainDeletions", func(t *testing.T) { + testPostgresIndexer(t, true) + }) + t.Run("NoRetainDeletions", func(t *testing.T) { + testPostgresIndexer(t, false) + }) +} + +func testPostgresIndexer(t *testing.T, retainDeletions bool) { + tempDir, err := os.MkdirTemp("", "postgres-indexer-test") + require.NoError(t, err) + dbPort := freeport.GetOne(t) - pgConfig := embeddedpostgres.DefaultConfig().Port(uint32(dbPort)) + pgConfig := embeddedpostgres.DefaultConfig(). + Port(uint32(dbPort)). + DataPath(tempDir) + dbUrl := pgConfig.GetConnectionURL() pg := embeddedpostgres.NewDatabase(pgConfig) require.NoError(t, pg.Start()) @@ -27,14 +49,65 @@ func TestPostgresIndexer(t *testing.T) { t.Cleanup(func() { cancel() require.NoError(t, pg.Stop()) + err := os.RemoveAll(tempDir) + require.NoError(t, err) }) - indexer, err := postgres.NewIndexer(ctx, postgres.Options{ - Driver: "pgx", - ConnectionURL: dbUrl, + db, err := sql.Open("pgx", dbUrl) + require.NoError(t, err) + + indexer, err := postgres.NewIndexer(db, postgres.Options{ + RetainDeletions: retainDeletions, + Logger: log.NewTestLogger(t), }) require.NoError(t, err) - fixture := indexertesting.NewListenerTestFixture(indexer.Listener(), indexertesting.ListenerTestFixtureOptions{}) + res, err := indexer.Initialize(ctx, indexing.InitializationData{}) + require.NoError(t, err) + + fixture := appdatatest.NewSimulator(appdatatest.SimulatorOptions{ + Listener: appdata.ListenerMux( + appdata.DebugListener(os.Stdout), + res.Listener, + ), + AppSchema: indexertesting.ExampleAppSchema, + StateSimOptions: statesim.Options{ + CanRetainDeletions: retainDeletions, + }, + }) + require.NoError(t, fixture.Initialize()) + + blockDataGen := fixture.BlockDataGenN(1000) + for i := 0; i < 1000; i++ { + blockData := blockDataGen.Example(i) + require.NoError(t, fixture.ProcessBlockData(blockData)) + + require.NoError(t, fixture.AppState().ScanObjectCollections(func(moduleName string, collection *statesim.ObjectCollection) error { + modMgr, ok := indexer.Modules()[moduleName] + require.True(t, ok) + tblMgr, ok := modMgr.Tables[collection.ObjectType().Name] + require.True(t, ok) + + expectedCount := collection.Len() + actualCount, err := tblMgr.Count(context.Background(), indexer.ActiveTx()) + require.NoError(t, err) + require.Equalf(t, expectedCount, actualCount, "table %s %s count mismatch", moduleName, collection.ObjectType().Name) + + return collection.ScanState(func(update schema.ObjectUpdate) error { + found, err := tblMgr.Equals( + context.Background(), + indexer.ActiveTx(), update.Key, update.Value) + if err != nil { + return err + } + + if !found { + return fmt.Errorf("object not found in table %s %s %v %v", moduleName, collection.ObjectType().Name, update.Key, update.Value) + } + + return nil + }) + })) + } } diff --git a/indexer/postgres/where.go b/indexer/postgres/where.go new file mode 100644 index 000000000000..81e63fc20de6 --- /dev/null +++ b/indexer/postgres/where.go @@ -0,0 +1,58 @@ +package postgres + +import ( + "fmt" + "io" +) + +func (tm *TableManager) WhereSqlAndParams(w io.Writer, key interface{}, startParamIdx int) (endParamIdx int, keyParams []interface{}, err error) { + var keyCols []string + keyParams, keyCols, err = tm.bindKeyParams(key) + if err != nil { + return + } + + endParamIdx, keyParams, err = tm.WhereSql(w, keyParams, keyCols, startParamIdx) + return +} + +func (tm *TableManager) WhereSql(w io.Writer, params []interface{}, cols []string, startParamIdx int) (endParamIdx int, resParams []interface{}, err error) { + _, err = fmt.Fprintf(w, " WHERE ") + if err != nil { + return + } + + endParamIdx = startParamIdx + for i, col := range cols { + if i > 0 { + _, err = fmt.Fprintf(w, " AND ") + if err != nil { + return + } + } + + _, err = fmt.Fprintf(w, "%s ", col) + if err != nil { + return + } + + if params[i] == nil { + _, err = fmt.Fprintf(w, "IS NULL") + if err != nil { + return + } + + } else { + _, err = fmt.Fprintf(w, "= $%d", endParamIdx) + if err != nil { + return + } + + resParams = append(resParams, params[i]) + + endParamIdx++ + } + } + + return +} diff --git a/indexer/testing/README.md b/indexer/testing/README.md deleted file mode 100644 index 91921cac8c14..000000000000 --- a/indexer/testing/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Indexer Testing - -This module contains core test utilities and fixtures for testing indexer functionality. It is managed as a separate go module to manage versions better and allow for dependencies on useful testing libraries without imposing those -elsewhere. \ No newline at end of file diff --git a/indexer/testing/fixture.go b/indexer/testing/fixture.go deleted file mode 100644 index 45fc57d5df0f..000000000000 --- a/indexer/testing/fixture.go +++ /dev/null @@ -1,315 +0,0 @@ -package indexertesting - -import ( - "encoding/json" - "fmt" - rand "math/rand/v2" - "time" - - "github.com/brianvoe/gofakeit/v7" - - indexerbase "cosmossdk.io/indexer/base" -) - -// ListenerTestFixture is a test fixture for testing listener implementations with a pre-defined data set -// that attempts to cover all known types of objects and fields. The test data currently includes data for -// two fake modules over three blocks of data. The data set should remain relatively stable between releases -// and generally only be changed when new features are added, so it should be suitable for regression or golden tests. -type ListenerTestFixture struct { - rndSource rand.Source - block uint64 - listener indexerbase.Listener - allKeyModule *testModule -} - -type ListenerTestFixtureOptions struct { - EventAlignedWrites bool -} - -func NewListenerTestFixture(listener indexerbase.Listener, options ListenerTestFixtureOptions) *ListenerTestFixture { - src := rand.NewPCG(1, 2) - return &ListenerTestFixture{ - rndSource: src, - listener: listener, - allKeyModule: mkAllKeysModule(src), - } -} - -func (f *ListenerTestFixture) Initialize() error { - if f.listener.InitializeModuleSchema != nil { - err := f.listener.InitializeModuleSchema(f.allKeyModule.name, f.allKeyModule.schema) - if err != nil { - return err - } - } - return nil -} - -func (f *ListenerTestFixture) NextBlock() error { - // TODO: - //f.block++ - // - //if f.listener.StartBlock != nil { - // err := f.listener.StartBlock(f.block) - // if err != nil { - // return err - // } - //} - // - //err := f.allKeyModule.updater(f.rndSource, &f.listener) - //if err != nil { - // return err - //} - // - //if f.listener.Commit != nil { - // err := f.listener.Commit() - // if err != nil { - // return err - // } - //} - - return nil -} - -func (f *ListenerTestFixture) block3() error { - return nil -} - -var moduleSchemaA = indexerbase.ModuleSchema{ - ObjectTypes: []indexerbase.ObjectType{ - { - "Singleton", - []indexerbase.Field{}, - []indexerbase.Field{ - { - Name: "Value", - Kind: indexerbase.StringKind, - }, - }, - false, - }, - { - Name: "Simple", - KeyFields: []indexerbase.Field{ - { - Name: "Key", - Kind: indexerbase.StringKind, - }, - }, - ValueFields: []indexerbase.Field{ - { - Name: "Value1", - Kind: indexerbase.Int32Kind, - }, - { - Name: "Value2", - Kind: indexerbase.BytesKind, - }, - }, - }, - { - Name: "Two Keys", - KeyFields: []indexerbase.Field{ - { - Name: "Key1", - Kind: indexerbase.StringKind, - }, - { - Name: "Key2", - Kind: indexerbase.Int32Kind, - }, - }, - }, - { - Name: "Main Values", - }, - { - Name: "No Values", - }, - }, -} - -var maxKind = indexerbase.JSONKind - -type testModule struct { - name string - schema indexerbase.ModuleSchema - state map[string]*testObjectStore -} - -type testObjectStore struct { - updater func(rand.Source, *indexerbase.Listener) error - state map[string]kvPair -} - -type kvPair struct { - key any - value any - state valueState -} - -type valueState int - -const ( - valueStateNotInitialized valueState = iota - valueStateSet - valueStateDeleted -) - -func mkAllKeysModule(src rand.Source) *testModule { - mod := &testModule{ - name: "all_keys", - state: map[string]*testObjectStore{}, - } - for i := 1; i < int(maxKind); i++ { - kind := indexerbase.Kind(i) - typ := mkTestObjectType(kind) - mod.schema.ObjectTypes = append(mod.schema.ObjectTypes, typ) - state := map[string]kvPair{} - // generate 5 keys - for j := 0; j < 5; j++ { - key1 := mkTestValue(src, kind, false) - key2 := mkTestValue(src, kind, true) - key := []any{key1, key2} - state[fmt.Sprintf("%v", key)] = kvPair{ - key: key, - } - } - - objStore := &testObjectStore{ - state: state, - } - mod.state[typ.Name] = objStore - } - - return mod -} - -func mkTestObjectType(kind indexerbase.Kind) indexerbase.ObjectType { - field := indexerbase.Field{ - Kind: kind, - } - - if kind == indexerbase.EnumKind { - field.EnumDefinition = testEnum - } - - if kind == indexerbase.Bech32AddressKind { - field.AddressPrefix = "cosmos" - } - - key1Field := field - key1Field.Name = "keyNotNull" - key2Field := field - key2Field.Name = "keyNullable" - key2Field.Nullable = true - val1Field := field - val1Field.Name = "valNotNull" - val2Field := field - val2Field.Name = "valNullable" - val2Field.Nullable = true - - return indexerbase.ObjectType{ - Name: fmt.Sprintf("test_%v", kind), - KeyFields: []indexerbase.Field{key1Field, key2Field}, - ValueFields: []indexerbase.Field{val1Field, val2Field}, - } -} - -func mkTestUpdate(rnd rand.Source, kind indexerbase.Kind) indexerbase.ObjectUpdate { - update := indexerbase.ObjectUpdate{} - - k1 := mkTestValue(rnd, kind, false) - k2 := mkTestValue(rnd, kind, true) - update.Key = []any{k1, k2} - - // delete 50% of the time - if rnd.Uint64()%2 == 0 { - update.Delete = true - return update - } - - v1 := mkTestValue(rnd, kind, false) - v2 := mkTestValue(rnd, kind, true) - update.Value = []any{v1, v2} - - return update -} - -func mkTestValue(src rand.Source, kind indexerbase.Kind, nullable bool) any { - faker := gofakeit.NewFaker(src, false) - // if it's nullable, return nil 50% of the time - if nullable && faker.Bool() { - return nil - } - - switch kind { - case indexerbase.StringKind: - // TODO fmt.Stringer - return faker.LoremIpsumSentence(faker.IntN(100)) - case indexerbase.BytesKind: - return randBytes(src) - case indexerbase.Int8Kind: - return faker.Int8() - case indexerbase.Int16Kind: - return faker.Int16() - case indexerbase.Uint8Kind: - return faker.Uint16() - case indexerbase.Uint16Kind: - return faker.Uint16() - case indexerbase.Int32Kind: - return faker.Int32() - case indexerbase.Uint32Kind: - return faker.Uint32() - case indexerbase.Int64Kind: - return faker.Int64() - case indexerbase.Uint64Kind: - return faker.Uint64() - case indexerbase.IntegerKind: - x := faker.Int64() - return fmt.Sprintf("%d", x) - case indexerbase.DecimalKind: - x := faker.Int64() - y := faker.UintN(1000000) - return fmt.Sprintf("%d.%d", x, y) - case indexerbase.BoolKind: - return faker.Bool() - case indexerbase.TimeKind: - return time.Unix(faker.Int64(), int64(faker.UintN(1000000000))) - case indexerbase.DurationKind: - return time.Duration(faker.Int64()) - case indexerbase.Float32Kind: - return faker.Float32() - case indexerbase.Float64Kind: - return faker.Float64() - case indexerbase.Bech32AddressKind: - // TODO: select from some actually valid known bech32 address strings and bytes" - return "cosmos1abcdefgh1234567890" - case indexerbase.EnumKind: - return faker.RandomString(testEnum.Values) - case indexerbase.JSONKind: - // TODO: other types - bz, err := faker.JSON(nil) - if err != nil { - panic(err) - } - return json.RawMessage(bz) - default: - } - panic(fmt.Errorf("unexpected kind: %v", kind)) -} - -func randBytes(src rand.Source) []byte { - rnd := rand.New(src) - n := rnd.IntN(1024) - bz := make([]byte, n) - for i := 0; i < n; i++ { - bz[i] = byte(rnd.Uint32N(256)) - } - return bz -} - -var testEnum = indexerbase.EnumDefinition{ - Name: "TestEnum", - Values: []string{"A", "B", "C"}, -} diff --git a/indexer/testing/fixture_test.go b/indexer/testing/fixture_test.go deleted file mode 100644 index 7204bda14767..000000000000 --- a/indexer/testing/fixture_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package indexertesting - -import "testing" - -func TestListenerFixture(t *testing.T) { - fixture := NewListenerTestFixture(StdoutListener(), ListenerTestFixtureOptions{}) - - err := fixture.Initialize() - if err != nil { - t.Fatal(err) - } - - err = fixture.NextBlock() - if err != nil { - t.Fatal(err) - } -} diff --git a/indexer/testing/go.mod b/indexer/testing/go.mod deleted file mode 100644 index 884bd5f95f51..000000000000 --- a/indexer/testing/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module cosmossdk.io/indexer/testing - -require ( - cosmossdk.io/indexer/base v0.0.0 - github.com/brianvoe/gofakeit/v7 v7.0.3 -) - -replace cosmossdk.io/indexer/base => ../base - -go 1.22 diff --git a/indexer/testing/go.sum b/indexer/testing/go.sum deleted file mode 100644 index 9bf55d041413..000000000000 --- a/indexer/testing/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/brianvoe/gofakeit/v7 v7.0.3 h1:tGCt+eYfhTMWE1ko5G2EO1f/yE44yNpIwUb4h32O0wo= -github.com/brianvoe/gofakeit/v7 v7.0.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= diff --git a/indexer/testing/test_listener.go b/indexer/testing/test_listener.go deleted file mode 100644 index 26bccc118f58..000000000000 --- a/indexer/testing/test_listener.go +++ /dev/null @@ -1,45 +0,0 @@ -package indexertesting - -import ( - "fmt" - "io" - "os" - - indexerbase "cosmossdk.io/indexer/base" -) - -func StdoutListener() indexerbase.Listener { - return WriterListener(os.Stdout) -} - -func WriterListener(w io.Writer) indexerbase.Listener { - return indexerbase.Listener{ - Initialize: func(data indexerbase.InitializationData) (lastBlockPersisted int64, err error) { - _, err = fmt.Fprintf(w, "Initialize: %v\n", data) - return 0, err - }, - StartBlock: func(u uint64) error { - _, err := fmt.Fprintf(w, "StartBlock: %d\n", u) - return err - }, - OnBlockHeader: func(data indexerbase.BlockHeaderData) error { - _, err := fmt.Fprintf(w, "OnBlockHeader: %v\n", data) - return err - }, - OnTx: nil, - OnEvent: nil, - OnKVPair: nil, - Commit: func() error { - _, err := fmt.Fprintf(w, "Commit\n") - return err - }, - InitializeModuleSchema: func(moduleName string, schema indexerbase.ModuleSchema) error { - _, err := fmt.Fprintf(w, "InitializeModuleSchema: %s %v\n", moduleName, schema) - return err - }, - OnObjectUpdate: func(moduleName string, data indexerbase.ObjectUpdate) error { - _, err := fmt.Fprintf(w, "OnObjectUpdate: %s: %v\n", moduleName, data) - return err - }, - } -} diff --git a/indexer/base/CHANGELOG.md b/schema/CHANGELOG.md similarity index 100% rename from indexer/base/CHANGELOG.md rename to schema/CHANGELOG.md diff --git a/schema/README.md b/schema/README.md new file mode 100644 index 000000000000..316a57e67386 --- /dev/null +++ b/schema/README.md @@ -0,0 +1,13 @@ +# Logical State Schema Framework + +The `cosmossdk.io/schema` base module is designed to provide a stable, **zero-dependency** base layer for specifying the **logical representation of module state schemas** and implementing **state indexing**. This is intended to be used primarily for indexing modules in external databases and providing a standard human-readable state representation for genesis import and export. + +The schema defined in this library does not aim to be general purpose and cover all types of schemas, such as those used for defining transactions. For instance, this schema is not include many types of composite objects as nested objects and arrays. Rather, the schema defined here aims to cover _state_ schemas only which are implemented as key-value pairs and usually have direct mappings to relational database tables or objects in a document store. + +Also, this schema does not cover physical state layout and byte-level encoding, but simply describes a common logical format. + +## `HasModuleCodec` Interface + +Any module which supports logical decoding and/or encoding should implement the `HasModuleCodec` interface. This interface provides a way to get the codec for the module, which can be used to decode the module's state and/or apply logical updates. + +State frameworks such as `collections` or `orm` should directly provide `ModuleCodec` implementations so that this functionality basically comes for free if a compatible framework is used. Modules that do not use one of these frameworks can choose to manually implement logical decoding and/or encoding. diff --git a/schema/appdata/README.md b/schema/appdata/README.md new file mode 100644 index 000000000000..d750e42efa56 --- /dev/null +++ b/schema/appdata/README.md @@ -0,0 +1,32 @@ +# Listener + +The `listener` package defines the basic types for streaming blockchain event and state data to external listeners, with a specific focus on supporting logical decoding and indexing of state. + +A blockchain data source should accept a `Listener` instance and invoke the provided callbacks in the correct order. A downstream listener should provide a `Listener` instance and perform operations based on the data passed to its callbacks. + +## `Listener` Callback Order + +`Listener` callbacks should be called in this order + +```mermaid +sequenceDiagram + actor Source + actor Target + Source ->> Target: Initialize + Source -->> Target: InitializeModuleSchema + loop Block + Source ->> Target: StartBlock + Source ->> Target: OnBlockHeader + Source -->> Target: OnTx + Source -->> Target: OnEvent + Source -->> Target: OnKVPair + Source -->> Target: OnObjectUpdate + Source ->> Target: Commit + end +``` + +`Initialize` must be called before any other method and should only be invoked once. `InitializeModuleSchema` should be called at most once for every module with logical data. + +Sources will generally only call `InitializeModuleSchema` and `OnObjectUpdate` if they have native logical decoding capabilities. Usually, the indexer framework will provide this functionality based on `OnKVPair` data and `schema.HasModuleCodec` implementations. + +`StartBlock` and `OnBlockHeader` should be called only once at the beginning of a block, and `Commit` should be called only once at the end of a block. The `OnTx`, `OnEvent`, `OnKVPair` and `OnObjectUpdate` must be called after `OnBlockHeader`, may be called multiple times within a block and indexers should not assume that the order is logical unless `InitializationData.HasEventAlignedWrites` is true. diff --git a/schema/appdata/async.go b/schema/appdata/async.go new file mode 100644 index 000000000000..47ffc707fe54 --- /dev/null +++ b/schema/appdata/async.go @@ -0,0 +1,115 @@ +package appdata + +func AsyncListener(listener Listener, bufferSize int, commitChan chan<- error, doneChan <-chan struct{}) Listener { + packetChan := make(chan Packet, bufferSize) + res := Listener{} + + go func() { + var err error + for { + select { + case packet := <-packetChan: + if err != nil { + // if we have an error, don't process any more packets + // and return the error and finish when it's time to commit + if _, ok := packet.(CommitData); ok { + commitChan <- err + return + } + } else { + // process the packet + err = listener.SendPacket(packet) + // if it's a commit + if _, ok := packet.(CommitData); ok { + commitChan <- err + if err != nil { + return + } + } + } + + case <-doneChan: + return + } + } + }() + + if listener.InitializeModuleData != nil { + res.InitializeModuleData = func(data ModuleInitializationData) error { + packetChan <- data + return nil + } + } + + if listener.StartBlock != nil { + res.StartBlock = func(data StartBlockData) error { + packetChan <- data + return nil + } + } + + if listener.OnTx != nil { + res.OnTx = func(data TxData) error { + packetChan <- data + return nil + } + } + + if listener.OnEvent != nil { + res.OnEvent = func(data EventData) error { + packetChan <- data + return nil + } + } + + if listener.OnKVPair != nil { + res.OnKVPair = func(data KVPairData) error { + packetChan <- data + return nil + } + } + + if listener.OnObjectUpdate != nil { + res.OnObjectUpdate = func(data ObjectUpdateData) error { + packetChan <- data + return nil + } + } + + if listener.Commit != nil { + res.Commit = func(data CommitData) error { + packetChan <- data + return nil + } + } + + return res +} + +func AsyncListenerMux(listeners []Listener, bufferSize int, doneChan <-chan struct{}) Listener { + asyncListeners := make([]Listener, len(listeners)) + commitChans := make([]chan error, len(listeners)) + for i, l := range listeners { + commitChan := make(chan error) + commitChans[i] = commitChan + asyncListeners[i] = AsyncListener(l, bufferSize, commitChan, doneChan) + } + mux := ListenerMux(asyncListeners...) + muxCommit := mux.Commit + mux.Commit = func(data CommitData) error { + err := muxCommit(data) + if err != nil { + return err + } + + for _, commitChan := range commitChans { + err := <-commitChan + if err != nil { + return err + } + } + return nil + } + + return mux +} diff --git a/schema/appdata/data.go b/schema/appdata/data.go new file mode 100644 index 000000000000..1712c623b5fa --- /dev/null +++ b/schema/appdata/data.go @@ -0,0 +1,99 @@ +package appdata + +import ( + "encoding/json" + + "cosmossdk.io/schema" +) + +// InitializationData represents initialization data that is passed to a listener. +type InitializationData struct { + // HasEventAlignedWrites indicates that the blockchain data source will emit KV-pair events + // in an order aligned with transaction, message and event callbacks. If this is true + // then indexers can assume that KV-pair data is associated with these specific transactions, messages + // and events. This may be useful for indexers which store a log of all operations (such as immutable + // or version controlled databases) so that the history log can include fine grain correlation between + // state updates and transactions, messages and events. If this value is false, then indexers should + // assume that KV-pair data occurs out of order with respect to transaction, message and event callbacks - + // the only safe assumption being that KV-pair data is associated with the block in which it was emitted. + HasEventAlignedWrites bool +} + +type ModuleInitializationData struct { + ModuleName string + Schema schema.ModuleSchema +} + +type StartBlockData struct { + // Height is the height of the block. + Height uint64 + + // Bytes is the raw byte representation of the block header. + HeaderBytes ToBytes + + // JSON is the JSON representation of the block header. It should generally be a JSON object. + HeaderJSON ToJSON +} + +// TxData represents the raw transaction data that is passed to a listener. +type TxData struct { + // TxIndex is the index of the transaction in the block. + TxIndex int32 + + // Bytes is the raw byte representation of the transaction. + Bytes ToBytes + + // JSON is the JSON representation of the transaction. It should generally be a JSON object. + JSON ToJSON +} + +// EventData represents event data that is passed to a listener. +type EventData struct { + // TxIndex is the index of the transaction in the block to which this event is associated. + // It should be set to a negative number if the event is not associated with a transaction. + // Canonically -1 should be used to represent begin block processing and -2 should be used to + // represent end block processing. + TxIndex int32 + + // MsgIndex is the index of the message in the transaction to which this event is associated. + // If TxIndex is negative, this index could correspond to the index of the message in + // begin or end block processing if such indexes exist, or it can be set to zero. + MsgIndex uint32 + + // EventIndex is the index of the event in the message to which this event is associated. + EventIndex uint32 + + // Type is the type of the event. + Type string + + // Data is the JSON representation of the event data. It should generally be a JSON object. + Data ToJSON +} + +// ToBytes is a function that lazily returns the raw byte representation of data. +type ToBytes = func() ([]byte, error) + +// ToJSON is a function that lazily returns the JSON representation of data. +type ToJSON = func() (json.RawMessage, error) + +type KVPairData struct { + Updates []ModuleKVPairUpdate +} + +type ModuleKVPairUpdate struct { + // ModuleName is the name of the module that the key-value pair belongs to. + ModuleName string + + Update schema.KVPairUpdate +} + +// ObjectUpdateData represents object update data that is passed to a listener. +type ObjectUpdateData struct { + // ModuleName is the name of the module that the update corresponds to. + ModuleName string + + // Updates are the object updates. + Updates []schema.ObjectUpdate +} + +type CommitData struct{} diff --git a/schema/appdata/debug.go b/schema/appdata/debug.go new file mode 100644 index 000000000000..bff6a851d533 --- /dev/null +++ b/schema/appdata/debug.go @@ -0,0 +1,31 @@ +package appdata + +import ( + "fmt" + "io" +) + +func DebugListener(out io.Writer) Listener { + res := packetForwarder(func(p Packet) error { + _, err := fmt.Fprintln(out, p) + return err + }) + //res.Initialize = func(ctx context.Context, data InitializationData) (lastBlockPersisted int64, err error) { + // _, err = fmt.Fprintf(out, "Initialize: %v\n", data) + // return 0, err + //} + return res +} + +func packetForwarder(f func(Packet) error) Listener { + return Listener{ + //Initialize: nil, // can't be forwarded + InitializeModuleData: func(data ModuleInitializationData) error { return f(data) }, + OnTx: func(data TxData) error { return f(data) }, + OnEvent: func(data EventData) error { return f(data) }, + OnKVPair: func(data KVPairData) error { return f(data) }, + OnObjectUpdate: func(data ObjectUpdateData) error { return f(data) }, + StartBlock: func(data StartBlockData) error { return f(data) }, + Commit: func(data CommitData) error { return f(data) }, + } +} diff --git a/schema/appdata/listener.go b/schema/appdata/listener.go new file mode 100644 index 000000000000..e1c6f1662584 --- /dev/null +++ b/schema/appdata/listener.go @@ -0,0 +1,50 @@ +package appdata + +// Listener is an interface that defines methods for listening to both raw and logical blockchain data. +// It is valid for any of the methods to be nil, in which case the listener will not be called for that event. +// Listeners should understand the guarantees that are provided by the source they are listening to and +// understand which methods will or will not be called. For instance, most blockchains will not do logical +// decoding of data out of the box, so the InitializeModuleData and OnObjectUpdate methods will not be called. +// These methods will only be called when listening logical decoding is setup. +type Listener struct { + //// TODO: this is the only method that can't be packetized so muxing is a bit more complex - maybe we should pull this out and create a separate initialization mechanism + //// Initialize is called when the listener is initialized before any other methods are called. + //// The lastBlockPersisted return value should be the last block height the listener persisted if it is + //// persisting block data, 0 if it is not interested in persisting block data, or -1 if it is + //// persisting block data but has not persisted any data yet. This check allows the indexer + //// framework to ensure that the listener has not missed blocks. Data sources MUST call + //// initialize before any other method is called, otherwise, no data will be processed. + //Initialize func(context.Context, InitializationData) (lastBlockPersisted int64, err error) + + // InitializeModuleData should be called whenever the blockchain process starts OR whenever + // logical decoding of a module is initiated. An indexer listening to this event + // should ensure that they have performed whatever initialization steps (such as database + // migrations) required to receive OnObjectUpdate events for the given module. If the + // indexer's schema is incompatible with the module's on-chain schema, the listener should return + // an error. Module names must conform to the NameFormat regular expression. + InitializeModuleData func(ModuleInitializationData) error + + // StartBlock is called at the beginning of processing a block. + StartBlock func(StartBlockData) error + + // OnTx is called when a transaction is received. + OnTx func(TxData) error + + // OnEvent is called when an event is received. + OnEvent func(EventData) error + + // OnKVPair is called when a key-value has been written to the store for a given module. + // Module names must conform to the NameFormat regular expression. + OnKVPair func(updates KVPairData) error + + // OnObjectUpdate is called whenever an object is updated in a module's state. This is only called + // when logical data is available. It should be assumed that the same data in raw form + // is also passed to OnKVPair. Module names must conform to the NameFormat regular expression. + OnObjectUpdate func(ObjectUpdateData) error + + // Commit is called when state is committed, usually at the end of a block. Any + // indexers should commit their data when this is called and return an error if + // they are unable to commit. Data sources MUST call Commit when data is committed, + // otherwise it should be assumed that indexers have not persisted their state. + Commit func(CommitData) error +} diff --git a/schema/appdata/mux.go b/schema/appdata/mux.go new file mode 100644 index 000000000000..af2ffd9e3145 --- /dev/null +++ b/schema/appdata/mux.go @@ -0,0 +1,121 @@ +package appdata + +// ListenerMux returns a listener that forwards received events to all the provided listeners and only +// registers a callback if a non-nil callback is present in at least one of the listeners. +func ListenerMux(listeners ...Listener) Listener { + mux := Listener{} + + for _, l := range listeners { + if l.InitializeModuleData != nil { + mux.InitializeModuleData = func(data ModuleInitializationData) error { + for _, l := range listeners { + if l.InitializeModuleData != nil { + if err := l.InitializeModuleData(data); err != nil { + return err + } + } + } + return nil + } + break + } + } + + for _, l := range listeners { + if l.StartBlock != nil { + mux.StartBlock = func(data StartBlockData) error { + for _, l := range listeners { + if l.StartBlock != nil { + if err := l.StartBlock(data); err != nil { + return err + } + } + } + return nil + } + break + } + } + + for _, l := range listeners { + if l.OnTx != nil { + mux.OnTx = func(data TxData) error { + for _, l := range listeners { + if l.OnTx != nil { + if err := l.OnTx(data); err != nil { + return err + } + } + } + return nil + } + break + } + } + + for _, listener := range listeners { + if listener.OnEvent != nil { + mux.OnEvent = func(data EventData) error { + for _, l := range listeners { + if l.OnEvent != nil { + if err := l.OnEvent(data); err != nil { + return err + } + } + } + return nil + } + break + } + } + + for _, listener := range listeners { + if listener.OnKVPair != nil { + mux.OnKVPair = func(data KVPairData) error { + for _, l := range listeners { + if l.OnKVPair != nil { + if err := l.OnKVPair(data); err != nil { + return err + } + } + } + return nil + } + break + } + } + + for _, listener := range listeners { + if listener.OnObjectUpdate != nil { + mux.OnObjectUpdate = func(data ObjectUpdateData) error { + for _, l := range listeners { + if l.OnObjectUpdate != nil { + if err := l.OnObjectUpdate(data); err != nil { + return err + } + } + } + return nil + } + break + } + } + + for _, listener := range listeners { + if listener.Commit != nil { + mux.Commit = func(data CommitData) error { + for _, l := range listeners { + if l.Commit != nil { + if err := l.Commit(data); err != nil { + return err + } + } + } + return nil + } + break + } + } + + return mux +} diff --git a/schema/appdata/packet.go b/schema/appdata/packet.go new file mode 100644 index 000000000000..42f8d4b9972b --- /dev/null +++ b/schema/appdata/packet.go @@ -0,0 +1,58 @@ +package appdata + +type Packet interface { + apply(*Listener) error +} + +func (l Listener) SendPacket(p Packet) error { + return p.apply(&l) +} + +func (m ModuleInitializationData) apply(l *Listener) error { + if l.InitializeModuleData == nil { + return nil + } + return l.InitializeModuleData(m) +} + +func (b StartBlockData) apply(l *Listener) error { + if l.StartBlock == nil { + return nil + } + return l.StartBlock(b) +} + +func (t TxData) apply(l *Listener) error { + if l.OnTx == nil { + return nil + } + return l.OnTx(t) +} + +func (e EventData) apply(l *Listener) error { + if l.OnEvent == nil { + return nil + } + return l.OnEvent(e) +} + +func (k KVPairData) apply(l *Listener) error { + if l.OnKVPair == nil { + return nil + } + return l.OnKVPair(k) +} + +func (o ObjectUpdateData) apply(l *Listener) error { + if l.OnObjectUpdate == nil { + return nil + } + return l.OnObjectUpdate(o) +} + +func (c CommitData) apply(l *Listener) error { + if l.Commit == nil { + return nil + } + return l.Commit(c) +} diff --git a/schema/decoder.go b/schema/decoder.go new file mode 100644 index 000000000000..9165f1c04e40 --- /dev/null +++ b/schema/decoder.go @@ -0,0 +1,49 @@ +package schema + +import "context" + +// HasModuleCodec is an interface that modules can implement to provide a ModuleCodec. +// Usually these modules would also implement appmodule.AppModule, but that is not included +// to keep this package free of any dependencies. +type HasModuleCodec interface { + // ModuleCodec returns a ModuleCodec for the module. + ModuleCodec() (ModuleCodec, error) +} + +// ModuleCodec is a struct that contains the schema and a KVDecoder for a module. +type ModuleCodec struct { + // Schema is the schema for the module. It is required. + Schema ModuleSchema + + // KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. + // If it is nil, the module doesn't support state decoding directly. + KVDecoder KVDecoder + + // ApplyUpdate is a function that applies an ObjectUpdate to the module's state for the given context. + // If it is nil, the module doesn't support applying logical updates. If this function is provided + // then it can be used as a genesis import path. + ApplyUpdate ApplyUpdate +} + +// KVDecoder is a function that decodes a key-value pair into one or more ObjectUpdate's. +// If the KV-pair doesn't represent object updates, the function should return nil as the first +// and no error. The error result should only be non-nil when the decoder expected +// to parse a valid update and was unable to. In the case of an error, the decoder may return +// a non-nil value for the first return value, which can indicate which parts of the update +// were decodable to aid debugging. +type KVDecoder = func(KVPairUpdate) ([]ObjectUpdate, error) + +type KVPairUpdate struct { + // Key is the key of the key-value pair. + Key []byte + + // Value is the value of the key-value pair. It should be ignored when Delete is true. + Value []byte + + // Delete is a flag that indicates that the key-value pair was deleted. If it is false, + // then it is assumed that this has been a set operation. + Delete bool +} + +// ApplyUpdate is a function that applies an ObjectUpdate to the module's state for the given context. +type ApplyUpdate = func(context.Context, ObjectUpdate) error diff --git a/schema/decoding/README.md b/schema/decoding/README.md new file mode 100644 index 000000000000..21b5dd2fd2a5 --- /dev/null +++ b/schema/decoding/README.md @@ -0,0 +1,42 @@ +# Indexer Base + +The indexer base module is designed to provide a stable, zero-dependency base layer for the built-in indexer functionality. Packages that integrate with the indexer should feel free to depend on this package without fear of any external dependencies being pulled in. + +The basic types for specifying index sources, targets and decoders are provided here. An indexing source should accept a `Listener` instance and invoke the provided callbacks in the correct order. An indexer should provide a `Listener` instance and perform indexing operations based on the data passed to its callbacks. A module that exposes logical updates in the form of `ObjectUpdate`s should implement the `IndexableModule` interface. + +## `Listener` Callback Order + +`Listener` callbacks should be called in this order + +```mermaid +sequenceDiagram + actor Source + actor Manager + participant Indexer + Source -->> Manager: InitializeModuleSchema + Manager ->> Indexer: InitializeModuleSchema + Source ->> Manager: Initialize + Manager ->> Indexer: Initialize + loop Block + Source ->> Manager: StartBlock + Manager ->> Indexer: StartBlock + Source -->> Manager: OnBlockHeader + Manager -->> Indexer: OnBlockHeader + Source -->> Manager: OnTx + Manager -->> Indexer: OnTx + Source -->> Manager: OnEvent + Manager -->> Indexer: OnEvent + Source -->> Manager: OnKVPair + Manager -->> Indexer: OnKVPair + Source -->> Manager: OnObjectUpdate + Manager -->> Indexer: OnObjectUpdate + Source ->> Manager: Commit + Manager ->> Indexer: Commit + end +``` + +`InitializeModuleSchema` should be called at most once for every module with logical data and all calls to should happen even before `Initialize` is called. After that `Initialize` MUST be called before any other method and should only be invoked once. + +Sources will generally only call `InitializeModuleSchema` and `OnObjectUpdate` if they have native logical decoding capabilities. Usually, the indexer framework will provide this functionality based on `OnKVPair` data and `IndexableModule` implementations. + +`StartBlock` and `OnBlockHeader` should be called only once at the beginning of a block, and `Commit` should be called only once at the end of a block. The `OnTx`, `OnEvent`, `OnKVPair` and `OnObjectUpdate` must be called after `OnBlockHeader`, may be called multiple times within a block and indexers should not assume that the order is logical unless `InitializationData.HasEventAlignedWrites` is true. diff --git a/schema/decoding/middleware.go b/schema/decoding/middleware.go new file mode 100644 index 000000000000..3364a3bc9728 --- /dev/null +++ b/schema/decoding/middleware.go @@ -0,0 +1,87 @@ +package decoding + +import ( + "fmt" + + "cosmossdk.io/schema" + "cosmossdk.io/schema/appdata" + "cosmossdk.io/schema/log" +) + +type Options struct { + DecoderResolver DecoderResolver + + // SyncSource is the source that will be used do initial indexing of modules with pre-existing + // state. It is optional, but if it is not provided, indexing can only be starting when a node + // is synced from genesis. + SyncSource SyncSource + Logger log.Logger +} + +func Middleware(target appdata.Listener, opts Options) (appdata.Listener, error) { + initializeModuleData := target.InitializeModuleData + onKVPair := target.OnKVPair + + moduleCodecs := map[string]schema.ModuleCodec{} + if opts.DecoderResolver != nil { + err := opts.DecoderResolver.Iterate(func(moduleName string, codec schema.ModuleCodec) error { + opts.Logger.Info("Initializing module schema", "moduleName", moduleName) + + if _, ok := moduleCodecs[moduleName]; ok { + return fmt.Errorf("module %s already initialized", moduleName) + } + + if err := codec.Schema.Validate(); err != nil { + return fmt.Errorf("error validating schema for module %s: %w", moduleName, err) + } + + moduleCodecs[moduleName] = codec + if initializeModuleData != nil { + return initializeModuleData(appdata.ModuleInitializationData{ + ModuleName: moduleName, + Schema: codec.Schema, + }) + } + return nil + }) + if err != nil { + return appdata.Listener{}, err + } + } + + // TODO: catch-up sync + + target.OnKVPair = func(data appdata.KVPairData) error { + if onKVPair != nil { + return onKVPair(data) + } + + if target.OnObjectUpdate != nil { + for _, kvUpdate := range data.Updates { + codec, ok := moduleCodecs[kvUpdate.ModuleName] + if !ok { + // TODO handle discovering a new module + return nil + } + + updates, err := codec.KVDecoder(kvUpdate.Update) + if err != nil { + return err + } + + if !ok { + return nil + } + + return target.OnObjectUpdate(appdata.ObjectUpdateData{ + ModuleName: kvUpdate.ModuleName, + Updates: updates, + }) + } + } + + return nil + } + + return target, nil +} diff --git a/schema/decoding/resolver.go b/schema/decoding/resolver.go new file mode 100644 index 000000000000..ef2c720eb411 --- /dev/null +++ b/schema/decoding/resolver.go @@ -0,0 +1,71 @@ +package decoding + +import ( + "sort" + + "cosmossdk.io/schema" +) + +type DecoderResolver interface { + // Iterate iterates over all module decoders which should be initialized at startup. + Iterate(func(string, schema.ModuleCodec) error) error + + // LookupDecoder allows for resolving decoders dynamically. For instance, some module-like + // things may come into existence dynamically (like x/accounts or EVM or WASM contracts). + // The first time the manager sees one of these appearing in KV-store writes, it will + // lookup a decoder for it and cache it for future use. The manager will also perform + // a catch-up sync before passing any new writes to ensure that all historical state has + // been synced if there is any This check will only happen the first time a module is seen + // by the manager in a given process (a process restart will cause this check to happen again). + LookupDecoder(moduleName string) (decoder schema.ModuleCodec, found bool, err error) +} + +type moduleSetDecoderResolver struct { + moduleSet map[string]interface{} +} + +// ModuleSetDecoderResolver returns DecoderResolver that will discover modules implementing +// DecodeableModule in the provided module set. +func ModuleSetDecoderResolver(moduleSet map[string]interface{}) DecoderResolver { + return &moduleSetDecoderResolver{ + moduleSet: moduleSet, + } +} + +func (a moduleSetDecoderResolver) Iterate(f func(string, schema.ModuleCodec) error) error { + keys := make([]string, 0, len(a.moduleSet)) + for k := range a.moduleSet { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + module := a.moduleSet[k] + dm, ok := module.(schema.HasModuleCodec) + if ok { + decoder, err := dm.ModuleCodec() + if err != nil { + return err + } + err = f(k, decoder) + if err != nil { + return err + } + } + } + return nil +} + +func (a moduleSetDecoderResolver) LookupDecoder(moduleName string) (schema.ModuleCodec, bool, error) { + mod, ok := a.moduleSet[moduleName] + if !ok { + return schema.ModuleCodec{}, false, nil + } + + dm, ok := mod.(schema.HasModuleCodec) + if !ok { + return schema.ModuleCodec{}, false, nil + } + + decoder, err := dm.ModuleCodec() + return decoder, true, err +} diff --git a/schema/decoding/sync.go b/schema/decoding/sync.go new file mode 100644 index 000000000000..1bcd56fa0f89 --- /dev/null +++ b/schema/decoding/sync.go @@ -0,0 +1,8 @@ +package decoding + +// SyncSource is an interface that allows indexers to start indexing modules with pre-existing state. +type SyncSource interface { + + // IterateAllKVPairs iterates over all key-value pairs for a given module. + IterateAllKVPairs(moduleName string, fn func(key, value []byte) error) error +} diff --git a/schema/enum.go b/schema/enum.go new file mode 100644 index 000000000000..d70458d63889 --- /dev/null +++ b/schema/enum.go @@ -0,0 +1,50 @@ +package schema + +import "fmt" + +// EnumDefinition represents the definition of an enum type. +type EnumDefinition struct { + // Name is the name of the enum type. It must conform to the NameFormat regular expression. + // Its name must be unique between all enum types and object types in the module. + // The same enum, however, can be used in multiple object types and fields as long as the + // definition is identical each time + // TODO: uniqueness validation + Name string + + // Values is a list of distinct, non-empty values that are part of the enum type. + // Each value must conform to the NameFormat regular expression. + Values []string +} + +// Validate validates the enum definition. +func (e EnumDefinition) Validate() error { + if !ValidateName(e.Name) { + return fmt.Errorf("invalid enum definition name %q", e.Name) + } + + if len(e.Values) == 0 { + return fmt.Errorf("enum definition values cannot be empty") + } + seen := make(map[string]bool, len(e.Values)) + for i, v := range e.Values { + if !ValidateName(v) { + return fmt.Errorf("invalid enum definition value %q at index %d for enum %s", v, i, e.Name) + } + + if seen[v] { + return fmt.Errorf("duplicate enum definition value %q for enum %s", v, e.Name) + } + seen[v] = true + } + return nil +} + +// ValidateValue validates that the value is a valid enum value. +func (e EnumDefinition) ValidateValue(value string) error { + for _, v := range e.Values { + if v == value { + return nil + } + } + return fmt.Errorf("value %q is not a valid enum value for %s", value, e.Name) +} diff --git a/schema/enum_test.go b/schema/enum_test.go new file mode 100644 index 000000000000..435449d0c564 --- /dev/null +++ b/schema/enum_test.go @@ -0,0 +1,106 @@ +package schema + +import ( + "strings" + "testing" +) + +func TestEnumDefinition_Validate(t *testing.T) { + tests := []struct { + name string + enum EnumDefinition + errContains string + }{ + { + name: "valid enum", + enum: EnumDefinition{ + Name: "test", + Values: []string{"a", "b", "c"}, + }, + errContains: "", + }, + { + name: "empty name", + enum: EnumDefinition{ + Name: "", + Values: []string{"a", "b", "c"}, + }, + errContains: "invalid enum definition name", + }, + { + name: "empty values", + enum: EnumDefinition{ + Name: "test", + Values: []string{}, + }, + errContains: "enum definition values cannot be empty", + }, + { + name: "empty value", + enum: EnumDefinition{ + Name: "test", + Values: []string{"a", "", "c"}, + }, + errContains: "invalid enum definition value", + }, + { + name: "duplicate value", + enum: EnumDefinition{ + Name: "test", + Values: []string{"a", "b", "a"}, + }, + errContains: "duplicate enum definition value \"a\" for enum test", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.enum.Validate() + if tt.errContains == "" { + if err != nil { + t.Errorf("expected valid enum definition to pass validation, got: %v", err) + } + } else { + if err == nil { + t.Errorf("expected invalid enum definition to fail validation, got nil error") + } else if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("expected error to contain %s, got: %v", tt.errContains, err) + } + } + }) + } +} + +func TestEnumDefinition_ValidateValue(t *testing.T) { + enum := EnumDefinition{ + Name: "test", + Values: []string{"a", "b", "c"}, + } + + tests := []struct { + value string + errContains string + }{ + {"a", ""}, + {"b", ""}, + {"c", ""}, + {"d", "value \"d\" is not a valid enum value for test"}, + } + + for _, tt := range tests { + t.Run(tt.value, func(t *testing.T) { + err := enum.ValidateValue(tt.value) + if tt.errContains == "" { + if err != nil { + t.Errorf("expected valid enum value to pass validation, got: %v", err) + } + } else { + if err == nil { + t.Errorf("expected invalid enum value to fail validation, got nil error") + } else if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("expected error to contain %s, got: %v", tt.errContains, err) + } + } + }) + } +} diff --git a/schema/field.go b/schema/field.go new file mode 100644 index 000000000000..c537fc68df8c --- /dev/null +++ b/schema/field.go @@ -0,0 +1,82 @@ +package schema + +import "fmt" + +// Field represents a field in an object type. +type Field struct { + // Name is the name of the field. It must conform to the NameFormat regular expression. + Name string + + // Kind is the basic type of the field. + Kind Kind + + // Nullable indicates whether null values are accepted for the field. Key fields CANNOT be nullable. + Nullable bool + + // AddressPrefix is the address prefix of the field's kind, currently only used for Bech32AddressKind. + // TODO: add validation for valid address prefixes + AddressPrefix string + + // EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind. + // The same enum types can be reused in the same module schema, but they always must contain + // the same values for the same enum name. This possibly introduces some duplication of + // definitions but makes it easier to reason about correctness and validation in isolation. + EnumDefinition EnumDefinition + + // References is the name of another field in the same module schema that this field references. + // It must be in the form of ".", ex. "Account.address". + References string +} + +// Validate validates the field. +func (c Field) Validate() error { + // valid name + if !ValidateName(c.Name) { + return fmt.Errorf("invalid field name %q", c.Name) + } + + // valid kind + if err := c.Kind.Validate(); err != nil { + return fmt.Errorf("invalid field kind for %q: %w", c.Name, err) + } + + // address prefix only valid with Bech32AddressKind + if c.Kind == Bech32AddressKind && c.AddressPrefix == "" { + return fmt.Errorf("missing address prefix for field %q", c.Name) + } else if c.Kind != Bech32AddressKind && c.AddressPrefix != "" { + return fmt.Errorf("address prefix is only valid for field %q with type Bech32AddressKind", c.Name) + } + + // enum definition only valid with EnumKind + if c.Kind == EnumKind { + if err := c.EnumDefinition.Validate(); err != nil { + return fmt.Errorf("invalid enum definition for field %q: %w", c.Name, err) + } + } else if c.Kind != EnumKind && (c.EnumDefinition.Name != "" || c.EnumDefinition.Values != nil) { + return fmt.Errorf("enum definition is only valid for field %q with type EnumKind", c.Name) + } + + return nil +} + +// ValidateValue validates that the value conforms to the field's kind and nullability. +// Unlike Kind.ValidateValue, it also checks that the value conforms to the EnumDefinition +// if the field is an EnumKind. +func (c Field) ValidateValue(value interface{}) error { + if value == nil { + if !c.Nullable { + return fmt.Errorf("field %q cannot be null", c.Name) + } + return nil + } + err := c.Kind.ValidateValueType(value) + if err != nil { + return fmt.Errorf("invalid value for field %q: %w", c.Name, err) + } + + if c.Kind == EnumKind { + return c.EnumDefinition.ValidateValue(value.(string)) + } + + return nil +} diff --git a/schema/field_test.go b/schema/field_test.go new file mode 100644 index 000000000000..a1f8087fff6a --- /dev/null +++ b/schema/field_test.go @@ -0,0 +1,183 @@ +package schema + +import ( + "strings" + "testing" +) + +func TestField_Validate(t *testing.T) { + tests := []struct { + name string + field Field + errContains string + }{ + { + name: "valid field", + field: Field{ + Name: "field1", + Kind: StringKind, + }, + errContains: "", + }, + { + name: "empty name", + field: Field{ + Name: "", + Kind: StringKind, + }, + errContains: "invalid field name", + }, + { + name: "invalid kind", + field: Field{ + Name: "field1", + Kind: InvalidKind, + }, + errContains: "invalid field kind", + }, + { + name: "missing address prefix", + field: Field{ + Name: "field1", + Kind: Bech32AddressKind, + }, + errContains: "missing address prefix", + }, + { + name: "address prefix with non-Bech32AddressKind", + field: Field{ + Name: "field1", + Kind: StringKind, + AddressPrefix: "prefix", + }, + errContains: "address prefix is only valid for field \"field1\" with type Bech32AddressKind", + }, + { + name: "invalid enum definition", + field: Field{ + Name: "field1", + Kind: EnumKind, + }, + errContains: "invalid enum definition", + }, + { + name: "enum definition with non-EnumKind", + field: Field{ + Name: "field1", + Kind: StringKind, + EnumDefinition: EnumDefinition{Name: "enum"}, + }, + errContains: "enum definition is only valid for field \"field1\" with type EnumKind", + }, + { + name: "valid enum", + field: Field{ + Name: "field1", + Kind: EnumKind, + EnumDefinition: EnumDefinition{Name: "enum", Values: []string{"a", "b"}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.field.Validate() + if tt.errContains == "" { + if err != nil { + t.Errorf("expected no error, got: %v", err) + } + } else { + if err == nil { + t.Errorf("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("expected error contains: %s, got: %v", tt.errContains, err) + } + } + }) + } +} + +func TestField_ValidateValue(t *testing.T) { + tests := []struct { + name string + field Field + value interface{} + errContains string + }{ + { + name: "valid field", + field: Field{ + Name: "field1", + Kind: StringKind, + }, + value: "value", + errContains: "", + }, + { + name: "null non-nullable field", + field: Field{ + Name: "field1", + Kind: StringKind, + Nullable: false, + }, + value: nil, + errContains: "cannot be null", + }, + { + name: "null nullable field", + field: Field{ + Name: "field1", + Kind: StringKind, + Nullable: true, + }, + value: nil, + errContains: "", + }, + { + name: "invalid value", + field: Field{ + Name: "field1", + Kind: StringKind, + }, + value: 1, + errContains: "invalid value for field \"field1\"", + }, + { + name: "valid enum", + field: Field{ + Name: "field1", + Kind: EnumKind, + EnumDefinition: EnumDefinition{Name: "enum", Values: []string{"a", "b"}}, + }, + value: "a", + errContains: "", + }, + { + name: "invalid enum", + field: Field{ + Name: "field1", + Kind: EnumKind, + EnumDefinition: EnumDefinition{Name: "enum", Values: []string{"a", "b"}}, + }, + value: "c", + errContains: "not a valid enum value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.field.ValidateValue(tt.value) + if tt.errContains == "" { + if err != nil { + t.Errorf("expected no error, got: %v", err) + } + } else { + if err == nil { + t.Errorf("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("expected error contains: %s, got: %v", tt.errContains, err) + } + } + }) + } +} diff --git a/schema/fields.go b/schema/fields.go new file mode 100644 index 000000000000..1500d40bfa2a --- /dev/null +++ b/schema/fields.go @@ -0,0 +1,71 @@ +package schema + +import "fmt" + +// ValidateForKeyFields validates that the value conforms to the set of fields as a Key in an ObjectUpdate. +// See ObjectUpdate.Key for documentation on the requirements of such keys. +func ValidateForKeyFields(keyFields []Field, value interface{}) error { + return validateFieldsValue(keyFields, value) +} + +// ValidateForValueFields validates that the value conforms to the set of fields as a Value in an ObjectUpdate. +// See ObjectUpdate.Value for documentation on the requirements of such values. +func ValidateForValueFields(valueFields []Field, value interface{}) error { + valueUpdates, ok := value.(ValueUpdates) + if !ok { + return validateFieldsValue(valueFields, value) + } + + values := map[string]interface{}{} + err := valueUpdates.Iterate(func(fieldname string, value interface{}) bool { + values[fieldname] = value + return true + }) + if err != nil { + return err + } + + for _, field := range valueFields { + v, ok := values[field.Name] + if !ok { + continue + } + + if err := field.ValidateValue(v); err != nil { + return err + } + + delete(values, field.Name) + } + + if len(values) > 0 { + return fmt.Errorf("unexpected values in ValueUpdates: %v", values) + } + + return nil +} + +func validateFieldsValue(fields []Field, value interface{}) error { + if len(fields) == 0 { + return nil + } + + if len(fields) == 1 { + return fields[0].ValidateValue(value) + } + + values, ok := value.([]interface{}) + if !ok { + return fmt.Errorf("expected slice of values for key fields, got %T", value) + } + + if len(fields) != len(values) { + return fmt.Errorf("expected %d key fields, got %d values", len(fields), len(value.([]interface{}))) + } + for i, field := range fields { + if err := field.ValidateValue(values[i]); err != nil { + return err + } + } + return nil +} diff --git a/schema/fields_test.go b/schema/fields_test.go new file mode 100644 index 000000000000..aa22e02a1466 --- /dev/null +++ b/schema/fields_test.go @@ -0,0 +1,143 @@ +package schema + +import ( + "strings" + "testing" +) + +func TestValidateForKeyFields(t *testing.T) { + tests := []struct { + name string + keyFields []Field + key interface{} + errContains string + }{ + { + name: "no key fields", + keyFields: nil, + key: nil, + }, + { + name: "single key field, valid", + keyFields: object1Type.KeyFields, + key: "hello", + errContains: "", + }, + { + name: "single key field, invalid", + keyFields: object1Type.KeyFields, + key: []interface{}{"value"}, + errContains: "invalid value", + }, + { + name: "multiple key fields, valid", + keyFields: object2Type.KeyFields, + key: []interface{}{"hello", int32(42)}, + }, + { + name: "multiple key fields, not a slice", + keyFields: object2Type.KeyFields, + key: map[string]interface{}{"field1": "hello", "field2": "42"}, + errContains: "expected slice of values", + }, + { + name: "multiple key fields, wrong number of values", + keyFields: object2Type.KeyFields, + key: []interface{}{"hello"}, + errContains: "expected 2 key fields", + }, + { + name: "multiple key fields, invalid value", + keyFields: object2Type.KeyFields, + key: []interface{}{"hello", "abc"}, + errContains: "invalid value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateForKeyFields(tt.keyFields, tt.key) + if tt.errContains == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } else { + if err == nil || !strings.Contains(err.Error(), tt.errContains) { + t.Fatalf("expected error to contain %q, got: %v", tt.errContains, err) + } + } + }) + } +} + +func TestValidateForValueFields(t *testing.T) { + tests := []struct { + name string + valueFields []Field + value interface{} + errContains string + }{ + { + name: "no value fields", + valueFields: nil, + value: nil, + }, + { + name: "single value field, valid", + valueFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + }, + value: "hello", + errContains: "", + }, + { + name: "value updates, empty", + valueFields: object3Type.ValueFields, + value: MapValueUpdates(map[string]interface{}{}), + }, + { + name: "value updates, 1 field valid", + valueFields: object3Type.ValueFields, + value: MapValueUpdates(map[string]interface{}{ + "field1": "hello", + }), + }, + { + name: "value updates, 2 fields, 1 invalid", + valueFields: object3Type.ValueFields, + value: MapValueUpdates(map[string]interface{}{ + "field1": "hello", + "field2": "abc", + }), + errContains: "expected int32", + }, + { + name: "value updates, extra value", + valueFields: object3Type.ValueFields, + value: MapValueUpdates(map[string]interface{}{ + "field1": "hello", + "field2": int32(42), + "field3": "extra", + }), + errContains: "unexpected values", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateForValueFields(tt.valueFields, tt.value) + if tt.errContains == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } else { + if err == nil || !strings.Contains(err.Error(), tt.errContains) { + t.Fatalf("expected error to contain %q, got: %v", tt.errContains, err) + } + } + }) + } +} diff --git a/indexer/base/go.mod b/schema/go.mod similarity index 88% rename from indexer/base/go.mod rename to schema/go.mod index a373d4dc8377..054862393ff8 100644 --- a/indexer/base/go.mod +++ b/schema/go.mod @@ -1,4 +1,4 @@ -module cosmossdk.io/indexer/base +module cosmossdk.io/schema // NOTE: this go.mod should have zero dependencies and remain on go 1.12 to stay compatible // with all known production releases of the Cosmos SDK. This is to ensure that all historical diff --git a/schema/indexing/indexer.go b/schema/indexing/indexer.go new file mode 100644 index 000000000000..926392952153 --- /dev/null +++ b/schema/indexing/indexer.go @@ -0,0 +1,36 @@ +package indexing + +import ( + "context" + "fmt" + + "cosmossdk.io/schema/appdata" + "cosmossdk.io/schema/log" +) + +type Indexer interface { + Initialize(context.Context, InitializationData) (InitializationResult, error) +} + +type IndexerResources struct { + Logger log.Logger +} + +type IndexerFactory = func(options map[string]interface{}, resources IndexerResources) (Indexer, error) + +type InitializationData struct{} + +type InitializationResult struct { + Listener appdata.Listener + LastBlockPersisted int64 +} + +func RegisterIndexer(name string, factory IndexerFactory) { + if _, ok := indexerRegistry[name]; ok { + panic(fmt.Sprintf("indexer %s already registered", name)) + } + + indexerRegistry[name] = factory +} + +var indexerRegistry = map[string]IndexerFactory{} diff --git a/schema/indexing/logger.go b/schema/indexing/logger.go new file mode 100644 index 000000000000..900a8d89cebe --- /dev/null +++ b/schema/indexing/logger.go @@ -0,0 +1 @@ +package indexing diff --git a/schema/indexing/manager.go b/schema/indexing/manager.go new file mode 100644 index 000000000000..c858fbdc4ee8 --- /dev/null +++ b/schema/indexing/manager.go @@ -0,0 +1,70 @@ +package indexing + +import ( + "context" + "fmt" + + "cosmossdk.io/schema/appdata" + "cosmossdk.io/schema/decoding" + "cosmossdk.io/schema/log" +) + +type Options struct { + Context context.Context + Options map[string]interface{} + Resolver decoding.DecoderResolver + SyncSource decoding.SyncSource + Logger log.Logger +} + +func Start(opts Options) (appdata.Listener, error) { + if opts.Logger == nil { + opts.Logger = log.NoopLogger{} + } + + opts.Logger.Info("Starting Indexer Manager") + + resources := IndexerResources{Logger: opts.Logger} + + var indexers []appdata.Listener + ctx := opts.Context + if ctx == nil { + ctx = context.Background() + } + + for indexerName, factory := range indexerRegistry { + indexerOpts, ok := opts.Options[indexerName] + if !ok { + continue + } + + if opts.Logger != nil { + opts.Logger.Info(fmt.Sprintf("Starting Indexer %s", indexerName), "options", indexerOpts) + } + + optsMap, ok := indexerOpts.(map[string]interface{}) + if !ok { + return appdata.Listener{}, fmt.Errorf("invalid indexer options type %T for %s, expected a map", indexerOpts, indexerName) + } + + indexer, err := factory(optsMap, resources) + if err != nil { + return appdata.Listener{}, fmt.Errorf("failed to create indexer %s: %w", indexerName, err) + } + + res, err := indexer.Initialize(ctx, InitializationData{}) + if err != nil { + return appdata.Listener{}, fmt.Errorf("failed to initialize indexer %s: %w", indexerName, err) + } + + indexers = append(indexers, res.Listener) + + // TODO handle last block persisted + } + + return decoding.Middleware(appdata.AsyncListenerMux(indexers, 1024, ctx.Done()), decoding.Options{ + DecoderResolver: opts.Resolver, + SyncSource: opts.SyncSource, + Logger: opts.Logger, + }) +} diff --git a/indexer/base/kind.go b/schema/kind.go similarity index 59% rename from indexer/base/kind.go rename to schema/kind.go index be6351bf9442..9a27eed5cab0 100644 --- a/indexer/base/kind.go +++ b/schema/kind.go @@ -1,9 +1,11 @@ -package indexerbase +package schema import ( "encoding/json" "fmt" + "regexp" "time" + "unicode/utf8" ) // Kind represents the basic type of a field in an object. @@ -16,7 +18,8 @@ const ( InvalidKind Kind = iota // StringKind is a string type and values of this type must be of the go type string - // or implement fmt.Stringer(). + // containing valid UTF-8 and cannot contain null characters. + // TODO: add validation for null characters StringKind // BytesKind is a bytes type and values of this type must be of the go type []byte. @@ -46,16 +49,14 @@ const ( // Uint64Kind is a uint64 type and values of this type must be of the go type uint64. Uint64Kind - // IntegerKind represents an arbitrary precision integer number. Values of this type must - // be of the go type int64, string or a type that implements fmt.Stringer with the resulted string - // formatted as an integer number. - IntegerKind + // IntegerStringKind represents an arbitrary precision integer number. Values of this type must + // be of the go type string and formatted as base10 integers, specifically matching to + // the IntegerFormat regex. + IntegerStringKind - // DecimalKind represents an arbitrary precision decimal or integer number. Values of this type - // must be of the go type string or a type that implements fmt.Stringer with the resulting string - // formatted as decimal numbers with an optional fractional part. Exponential E-notation - // is supported but NaN and Infinity are not. - DecimalKind + // DecimalStringKind represents an arbitrary precision decimal or integer number. Values of this type + // must be of the go type string and match the DecimalFormat regex. + DecimalStringKind // BoolKind is a boolean type and values of this type must be of the go type bool. BoolKind @@ -72,22 +73,37 @@ const ( // Float64Kind is a float64 type and values of this type must be of the go type float64. Float64Kind - // Bech32AddressKind is a bech32 address type and values of this type must be of the go type string or []byte - // or a type which implements fmt.Stringer. Fields of this type are expected to set the AddressPrefix field - // in the field definition to the bech32 address prefix. + // Bech32AddressKind is a bech32 address type and values of this type must be of the go type []byte. + // Fields of this type are expected to set the AddressPrefix field in the field definition to the + // bech32 address prefix so that indexers can properly convert them to strings. Bech32AddressKind - // EnumKind is an enum type and values of this type must be of the go type string or implement fmt.Stringer. + // EnumKind is an enum type and values of this type must be of the go type string. // Fields of this type are expected to set the EnumDefinition field in the field definition to the enum // definition. EnumKind - // JSONKind is a JSON type and values of this type can either be of go type json.RawMessage - // or any type that can be marshaled to JSON using json.Marshal. + // JSONKind is a JSON type and values of this type should be of go type json.RawMessage and represent + // valid JSON. JSONKind ) -// Validate returns an error if the kind is invalid. +// MAX_VALID_KIND is the maximum valid kind value. +const MAX_VALID_KIND = JSONKind + +const ( + // IntegerFormat is a regex that describes the format integer number strings must match. It specifies + // that integers may have at most 100 digits. + IntegerFormat = `^-?[0-9]{1,100}$` + + // DecimalFormat is a regex that describes the format decimal number strings must match. It specifies + // that decimals may have at most 50 digits before and after the decimal point and may have an optional + // exponent of up to 2 digits. These restrictions ensure that the decimal can be accurately represented + // by a wide variety of implementations. + DecimalFormat = `^-?[0-9]{1,50}(\.[0-9]{1,50})?([eE][-+]?[0-9]{1,2})?$` +) + +// Validate returns an errContains if the kind is invalid. func (t Kind) Validate() error { if t <= InvalidKind { return fmt.Errorf("unknown type: %d", t) @@ -98,18 +114,65 @@ func (t Kind) Validate() error { return nil } -// ValidateValueType returns an error if the value does not the type go type specified by the kind. +// String returns a string representation of the kind. +func (t Kind) String() string { + switch t { + case StringKind: + return "string" + case BytesKind: + return "bytes" + case Int8Kind: + return "int8" + case Uint8Kind: + return "uint8" + case Int16Kind: + return "int16" + case Uint16Kind: + return "uint16" + case Int32Kind: + return "int32" + case Uint32Kind: + return "uint32" + case Int64Kind: + return "int64" + case Uint64Kind: + return "uint64" + case DecimalStringKind: + return "decimal" + case IntegerStringKind: + return "integer" + case BoolKind: + return "bool" + case TimeKind: + return "time" + case DurationKind: + return "duration" + case Float32Kind: + return "float32" + case Float64Kind: + return "float64" + case Bech32AddressKind: + return "bech32address" + case EnumKind: + return "enum" + case JSONKind: + return "json" + default: + return fmt.Sprintf("invalid(%d)", t) + } +} + +// ValidateValueType returns an errContains if the value does not conform to the expected go type. // Some fields may accept nil values, however, this method does not have any notion of -// nullability. It only checks that the value is of the correct type. -// It also doesn't perform any validation that IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind -// values are valid for their respective types. +// nullability. This method only validates that the go type of the value is correct for the kind +// and does not validate string or json formats. Kind.ValidateValue does a more thorough validation +// of number and json string formatting. func (t Kind) ValidateValueType(value interface{}) error { switch t { case StringKind: _, ok := value.(string) - _, ok2 := value.(fmt.Stringer) - if !ok && !ok2 { - return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + if !ok { + return fmt.Errorf("expected string, got %T", value) } case BytesKind: _, ok := value.([]byte) @@ -156,18 +219,16 @@ func (t Kind) ValidateValueType(value interface{}) error { if !ok { return fmt.Errorf("expected uint64, got %T", value) } - case IntegerKind: + case IntegerStringKind: _, ok := value.(string) - _, ok2 := value.(fmt.Stringer) - _, ok3 := value.(int64) - if !ok && !ok2 && !ok3 { - return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + if !ok { + return fmt.Errorf("expected string, got %T", value) } - case DecimalKind: + + case DecimalStringKind: _, ok := value.(string) - _, ok2 := value.(fmt.Stringer) - if !ok && !ok2 { - return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + if !ok { + return fmt.Errorf("expected string, got %T", value) } case BoolKind: _, ok := value.(bool) @@ -195,83 +256,69 @@ func (t Kind) ValidateValueType(value interface{}) error { return fmt.Errorf("expected float64, got %T", value) } case Bech32AddressKind: - _, ok := value.(string) - _, ok2 := value.([]byte) - _, ok3 := value.(fmt.Stringer) - if !ok && !ok2 && !ok3 { - return fmt.Errorf("expected string or []byte, got %T", value) + _, ok := value.([]byte) + if !ok { + return fmt.Errorf("expected []byte, got %T", value) } case EnumKind: _, ok := value.(string) - _, ok2 := value.(fmt.Stringer) - if !ok && !ok2 { - return fmt.Errorf("expected string or type that implements fmt.Stringer, got %T", value) + if !ok { + return fmt.Errorf("expected string, got %T", value) } case JSONKind: - return nil + _, ok := value.(json.RawMessage) + if !ok { + return fmt.Errorf("expected json.RawMessage, got %T", value) + } default: return fmt.Errorf("invalid type: %d", t) } return nil } -// String returns a string representation of the kind. -func (t Kind) String() string { +// ValidateValue returns an errContains if the value does not conform to the expected go type and format. +// It is more thorough, but slower, than Kind.ValidateValueType and validates that Integer, Decimal and JSON +// values are formatted correctly. It cannot validate enum values because Kind's do not have enum schemas. +func (t Kind) ValidateValue(value interface{}) error { + err := t.ValidateValueType(value) + if err != nil { + return err + } + switch t { case StringKind: - return "string" - case BytesKind: - return "bytes" - case Int8Kind: - return "int8" - case Uint8Kind: - return "uint8" - case Int16Kind: - return "int16" - case Uint16Kind: - return "uint16" - case Int32Kind: - return "int32" - case Uint32Kind: - return "uint32" - case Int64Kind: - return "int64" - case Uint64Kind: - return "uint64" - case DecimalKind: - return "decimal" - case IntegerKind: - return "integer" - case BoolKind: - return "bool" - case TimeKind: - return "time" - case DurationKind: - return "duration" - case Float32Kind: - return "float32" - case Float64Kind: - return "float64" - case Bech32AddressKind: - return "bech32address" - case EnumKind: - return "enum" + if !utf8.ValidString(value.(string)) { + return fmt.Errorf("expected valid utf-8 string, got %s", value) + } + case IntegerStringKind: + if !integerRegex.Match([]byte(value.(string))) { + return fmt.Errorf("expected base10 integer, got %s", value) + } + case DecimalStringKind: + if !decimalRegex.Match([]byte(value.(string))) { + return fmt.Errorf("expected decimal number, got %s", value) + } case JSONKind: - return "json" + if !json.Valid(value.(json.RawMessage)) { + return fmt.Errorf("expected valid JSON, got %s", value) + } default: - return "" + return nil } + return nil } +var ( + integerRegex = regexp.MustCompile(IntegerFormat) + decimalRegex = regexp.MustCompile(DecimalFormat) +) + // KindForGoValue finds the simplest kind that can represent the given go value. It will not, however, -// return kinds such as IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind which all can be -// represented as strings. Generally all values which do not have a more specific type will -// return JSONKind because the framework cannot decide at this point whether the value -// can or cannot be marshaled to JSON. This method should generally only be used as a fallback -// when the kind of a field is not specified more specifically. +// return kinds such as IntegerStringKind, DecimalStringKind, Bech32AddressKind, or EnumKind which all can be +// represented as strings. func KindForGoValue(value interface{}) Kind { switch value.(type) { - case string, fmt.Stringer: + case string: return StringKind case []byte: return BytesKind @@ -304,6 +351,6 @@ func KindForGoValue(value interface{}) Kind { case json.RawMessage: return JSONKind default: - return JSONKind + return InvalidKind } } diff --git a/schema/kind_test.go b/schema/kind_test.go new file mode 100644 index 000000000000..9027fbb70010 --- /dev/null +++ b/schema/kind_test.go @@ -0,0 +1,261 @@ +package schema + +import ( + "encoding/json" + "fmt" + "testing" + "time" +) + +func TestKind_Validate(t *testing.T) { + for kind := InvalidKind + 1; kind <= MAX_VALID_KIND; kind++ { + if err := kind.Validate(); err != nil { + t.Errorf("expected valid kind %s to pass validation, got: %v", kind, err) + } + } + + invalidKinds := []Kind{ + Kind(-1), + InvalidKind, + Kind(100), + } + + for _, kind := range invalidKinds { + if err := kind.Validate(); err == nil { + t.Errorf("expected invalid kind %s to fail validation, got: %v", kind, err) + } + } +} + +func TestKind_ValidateValueType(t *testing.T) { + tests := []struct { + kind Kind + value interface{} + valid bool + }{ + {kind: StringKind, value: "hello", valid: true}, + {kind: StringKind, value: []byte("hello"), valid: false}, + {kind: BytesKind, value: []byte("hello"), valid: true}, + {kind: BytesKind, value: "hello", valid: false}, + {kind: Int8Kind, value: int8(1), valid: true}, + {kind: Int8Kind, value: int16(1), valid: false}, + {kind: Uint8Kind, value: uint8(1), valid: true}, + {kind: Uint8Kind, value: uint16(1), valid: false}, + {kind: Int16Kind, value: int16(1), valid: true}, + {kind: Int16Kind, value: int32(1), valid: false}, + {kind: Uint16Kind, value: uint16(1), valid: true}, + {kind: Uint16Kind, value: uint32(1), valid: false}, + {kind: Int32Kind, value: int32(1), valid: true}, + {kind: Int32Kind, value: int64(1), valid: false}, + {kind: Uint32Kind, value: uint32(1), valid: true}, + {kind: Uint32Kind, value: uint64(1), valid: false}, + {kind: Int64Kind, value: int64(1), valid: true}, + {kind: Int64Kind, value: int32(1), valid: false}, + {kind: Uint64Kind, value: uint64(1), valid: true}, + {kind: Uint64Kind, value: uint32(1), valid: false}, + {kind: IntegerStringKind, value: "1", valid: true}, + {kind: IntegerStringKind, value: int32(1), valid: false}, + {kind: DecimalStringKind, value: "1.0", valid: true}, + {kind: DecimalStringKind, value: "1", valid: true}, + {kind: DecimalStringKind, value: "1.1e4", valid: true}, + {kind: DecimalStringKind, value: int32(1), valid: false}, + {kind: Bech32AddressKind, value: []byte("hello"), valid: true}, + {kind: Bech32AddressKind, value: 1, valid: false}, + {kind: BoolKind, value: true, valid: true}, + {kind: BoolKind, value: false, valid: true}, + {kind: BoolKind, value: 1, valid: false}, + {kind: EnumKind, value: "hello", valid: true}, + {kind: EnumKind, value: 1, valid: false}, + {kind: TimeKind, value: time.Now(), valid: true}, + {kind: TimeKind, value: "hello", valid: false}, + {kind: DurationKind, value: time.Second, valid: true}, + {kind: DurationKind, value: "hello", valid: false}, + {kind: Float32Kind, value: float32(1.0), valid: true}, + {kind: Float32Kind, value: float64(1.0), valid: false}, + {kind: Float64Kind, value: float64(1.0), valid: true}, + {kind: Float64Kind, value: float32(1.0), valid: false}, + {kind: JSONKind, value: json.RawMessage("{}"), valid: true}, + {kind: JSONKind, value: "hello", valid: false}, + {kind: InvalidKind, value: "hello", valid: false}, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { + err := tt.kind.ValidateValueType(tt.value) + if tt.valid && err != nil { + t.Errorf("test %d: expected valid value %v for kind %s to pass validation, got: %v", i, tt.value, tt.kind, err) + } + if !tt.valid && err == nil { + t.Errorf("test %d: expected invalid value %v for kind %s to fail validation, got: %v", i, tt.value, tt.kind, err) + } + }) + } + + // nils get rejected + for kind := InvalidKind + 1; kind <= MAX_VALID_KIND; kind++ { + if err := kind.ValidateValueType(nil); err == nil { + t.Errorf("expected nil value to fail validation for kind %s", kind) + } + } +} + +func TestKind_ValidateValue(t *testing.T) { + tests := []struct { + kind Kind + value interface{} + valid bool + }{ + // check a few basic cases that should get caught be ValidateValueType + {StringKind, "hello", true}, + {Int64Kind, int64(1), true}, + {Int32Kind, "abc", false}, + {BytesKind, nil, false}, + // check integer, decimal and json more thoroughly + {IntegerStringKind, "1", true}, + {IntegerStringKind, "0", true}, + {IntegerStringKind, "10", true}, + {IntegerStringKind, "-100", true}, + {IntegerStringKind, "1.0", false}, + {IntegerStringKind, "00", true}, // leading zeros are allowed + {IntegerStringKind, "001", true}, + {IntegerStringKind, "-01", true}, + // 100 digits + {IntegerStringKind, "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", true}, + // more than 100 digits + {IntegerStringKind, "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", false}, + {IntegerStringKind, "", false}, + {IntegerStringKind, "abc", false}, + {IntegerStringKind, "abc100", false}, + {DecimalStringKind, "1.0", true}, + {DecimalStringKind, "0.0", true}, + {DecimalStringKind, "-100.075", true}, + {DecimalStringKind, "1002346.000", true}, + {DecimalStringKind, "0", true}, + {DecimalStringKind, "10", true}, + {DecimalStringKind, "-100", true}, + {DecimalStringKind, "1", true}, + {DecimalStringKind, "1.0e4", true}, + {DecimalStringKind, "1.0e-4", true}, + {DecimalStringKind, "1.0e+4", true}, + {DecimalStringKind, "1.0e", false}, + {DecimalStringKind, "1.0e4.0", false}, + {DecimalStringKind, "1.0e-4.0", false}, + {DecimalStringKind, "1.0e+4.0", false}, + {DecimalStringKind, "-1.0e-4", true}, + {DecimalStringKind, "-1.0e+4", true}, + {DecimalStringKind, "-1.0E4", true}, + {DecimalStringKind, "1E-9", true}, + {DecimalStringKind, "1E-99", true}, + {DecimalStringKind, "1E+9", true}, + {DecimalStringKind, "1E+99", true}, + // 50 digits before and after the decimal point + {DecimalStringKind, "10000000000000000000000000000000000000000000000000.10000000000000000000000000000000000000000000000001", true}, + // too many digits before the decimal point + {DecimalStringKind, "10000000000000000000000000000000000000000000000000000000000000000000000000", false}, + // too many digits after the decimal point + {DecimalStringKind, "1.0000000000000000000000000000000000000000000000000000000000000000000000001", false}, + // exponent too big + {DecimalStringKind, "1E-999", false}, + {DecimalStringKind, "", false}, + {DecimalStringKind, "abc", false}, + {DecimalStringKind, "abc", false}, + {JSONKind, json.RawMessage(`{"a":10}`), true}, + {JSONKind, json.RawMessage("10"), true}, + {JSONKind, json.RawMessage("10.0"), true}, + {JSONKind, json.RawMessage("true"), true}, + {JSONKind, json.RawMessage("null"), true}, + {JSONKind, json.RawMessage(`"abc"`), true}, + {JSONKind, json.RawMessage(`[1,true,0.1,"abc",{"b":3}]`), true}, + {JSONKind, json.RawMessage(`"abc`), false}, + {JSONKind, json.RawMessage(`tru`), false}, + {JSONKind, json.RawMessage(`[`), false}, + {JSONKind, json.RawMessage(`{`), false}, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("test %v %s", tt.kind, tt.value), func(t *testing.T) { + err := tt.kind.ValidateValue(tt.value) + if tt.valid && err != nil { + t.Errorf("test %d: expected valid value %v for kind %s to pass validation, got: %v", i, tt.value, tt.kind, err) + } + if !tt.valid && err == nil { + t.Errorf("test %d: expected invalid value %v for kind %s to fail validation, got: %v", i, tt.value, tt.kind, err) + } + }) + } +} + +func TestKind_String(t *testing.T) { + tests := []struct { + kind Kind + want string + }{ + {StringKind, "string"}, + {BytesKind, "bytes"}, + {Int8Kind, "int8"}, + {Uint8Kind, "uint8"}, + {Int16Kind, "int16"}, + {Uint16Kind, "uint16"}, + {Int32Kind, "int32"}, + {Uint32Kind, "uint32"}, + {Int64Kind, "int64"}, + {Uint64Kind, "uint64"}, + {IntegerStringKind, "integer"}, + {DecimalStringKind, "decimal"}, + {BoolKind, "bool"}, + {TimeKind, "time"}, + {DurationKind, "duration"}, + {Float32Kind, "float32"}, + {Float64Kind, "float64"}, + {JSONKind, "json"}, + {EnumKind, "enum"}, + {Bech32AddressKind, "bech32address"}, + {InvalidKind, "invalid(0)"}, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("test %s", tt.kind), func(t *testing.T) { + if got := tt.kind.String(); got != tt.want { + t.Errorf("test %d: Kind.String() = %v, want %v", i, got, tt.want) + } + }) + } +} + +func TestKindForGoValue(t *testing.T) { + tests := []struct { + value interface{} + want Kind + }{ + {"hello", StringKind}, + {[]byte("hello"), BytesKind}, + {int8(1), Int8Kind}, + {uint8(1), Uint8Kind}, + {int16(1), Int16Kind}, + {uint16(1), Uint16Kind}, + {int32(1), Int32Kind}, + {uint32(1), Uint32Kind}, + {int64(1), Int64Kind}, + {uint64(1), Uint64Kind}, + {float32(1.0), Float32Kind}, + {float64(1.0), Float64Kind}, + {true, BoolKind}, + {time.Now(), TimeKind}, + {time.Second, DurationKind}, + {json.RawMessage("{}"), JSONKind}, + {map[string]interface{}{"a": 1}, InvalidKind}, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { + if got := KindForGoValue(tt.value); got != tt.want { + t.Errorf("test %d: KindForGoValue(%v) = %v, want %v", i, tt.value, got, tt.want) + } + + // for valid kinds check valid value + if tt.want.Validate() == nil { + if err := tt.want.ValidateValue(tt.value); err != nil { + t.Errorf("test %d: expected valid value %v for kind %s to pass validation, got: %v", i, tt.value, tt.want, err) + } + } + }) + } +} diff --git a/schema/log/logger.go b/schema/log/logger.go new file mode 100644 index 000000000000..2007d6cc5cc0 --- /dev/null +++ b/schema/log/logger.go @@ -0,0 +1,31 @@ +package log + +type Logger interface { + // Info takes a message and a set of key/value pairs and logs with level INFO. + // The key of the tuple must be a string. + Info(msg string, keyVals ...interface{}) + + // Warn takes a message and a set of key/value pairs and logs with level WARN. + // The key of the tuple must be a string. + Warn(msg string, keyVals ...interface{}) + + // Error takes a message and a set of key/value pairs and logs with level ERR. + // The key of the tuple must be a string. + Error(msg string, keyVals ...interface{}) + + // Debug takes a message and a set of key/value pairs and logs with level DEBUG. + // The key of the tuple must be a string. + Debug(msg string, keyVals ...interface{}) +} + +type NoopLogger struct{} + +func (n NoopLogger) Info(msg string, keyVals ...interface{}) {} + +func (n NoopLogger) Warn(msg string, keyVals ...interface{}) {} + +func (n NoopLogger) Error(msg string, keyVals ...interface{}) {} + +func (n NoopLogger) Debug(msg string, keyVals ...interface{}) {} + +var _ Logger = NoopLogger{} diff --git a/schema/module_schema.go b/schema/module_schema.go new file mode 100644 index 000000000000..170288b65023 --- /dev/null +++ b/schema/module_schema.go @@ -0,0 +1,74 @@ +package schema + +import "fmt" + +// ModuleSchema represents the logical schema of a module for purposes of indexing and querying. +type ModuleSchema struct { + // ObjectTypes describe the types of objects that are part of the module's schema. + ObjectTypes []ObjectType +} + +// Validate validates the module schema. +func (s ModuleSchema) Validate() error { + for _, objType := range s.ObjectTypes { + if err := objType.Validate(); err != nil { + return err + } + } + + // validate that shared enum types are consistent across object types + enumValueMap := map[string]map[string]bool{} + for _, objType := range s.ObjectTypes { + for _, field := range objType.KeyFields { + err := checkEnum(enumValueMap, field) + if err != nil { + return err + } + } + for _, field := range objType.ValueFields { + err := checkEnum(enumValueMap, field) + if err != nil { + return err + } + } + } + + return nil +} + +func checkEnum(enumValueMap map[string]map[string]bool, field Field) error { + if field.Kind != EnumKind { + return nil + } + + enum := field.EnumDefinition + + if existing, ok := enumValueMap[enum.Name]; ok { + if len(existing) != len(enum.Values) { + return fmt.Errorf("enum %q has different number of values in different object types", enum.Name) + } + + for _, value := range enum.Values { + if !existing[value] { + return fmt.Errorf("enum %q has different values in different object types", enum.Name) + } + } + } else { + valueMap := map[string]bool{} + for _, value := range enum.Values { + valueMap[value] = true + } + enumValueMap[enum.Name] = valueMap + } + return nil +} + +// ValidateObjectUpdate validates that the update conforms to the module schema. +func (s ModuleSchema) ValidateObjectUpdate(update ObjectUpdate) error { + for _, objType := range s.ObjectTypes { + if objType.Name == update.TypeName { + return objType.ValidateObjectUpdate(update) + } + } + return fmt.Errorf("object type %q not found in module schema", update.TypeName) +} diff --git a/schema/module_schema_test.go b/schema/module_schema_test.go new file mode 100644 index 000000000000..d2b6220a4f2c --- /dev/null +++ b/schema/module_schema_test.go @@ -0,0 +1,196 @@ +package schema + +import ( + "strings" + "testing" +) + +func TestModuleSchema_Validate(t *testing.T) { + tests := []struct { + name string + moduleSchema ModuleSchema + errContains string + }{ + { + name: "valid module schema", + moduleSchema: ModuleSchema{ + ObjectTypes: []ObjectType{ + { + Name: "object1", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + }, + }, + }, + }, + errContains: "", + }, + { + name: "invalid object type", + moduleSchema: ModuleSchema{ + ObjectTypes: []ObjectType{ + { + Name: "", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + }, + }, + }, + }, + errContains: "invalid object type name", + }, + { + name: "same enum with missing values", + moduleSchema: ModuleSchema{ + ObjectTypes: []ObjectType{ + { + Name: "object1", + KeyFields: []Field{ + { + Name: "k", + Kind: EnumKind, + EnumDefinition: EnumDefinition{ + Name: "enum1", + Values: []string{"a", "b"}, + }, + }, + }, + ValueFields: []Field{ + { + Name: "v", + Kind: EnumKind, + EnumDefinition: EnumDefinition{ + Name: "enum1", + Values: []string{"a", "b", "c"}, + }, + }, + }, + }, + }, + }, + errContains: "different number of values", + }, + { + name: "same enum with different values", + moduleSchema: ModuleSchema{ + ObjectTypes: []ObjectType{ + { + Name: "object1", + KeyFields: []Field{ + { + Name: "k", + Kind: EnumKind, + EnumDefinition: EnumDefinition{ + Name: "enum1", + Values: []string{"a", "b"}, + }, + }, + }, + }, + { + Name: "object2", + KeyFields: []Field{ + { + Name: "k", + Kind: EnumKind, + EnumDefinition: EnumDefinition{ + Name: "enum1", + Values: []string{"a", "c"}, + }, + }, + }, + }, + }, + }, + errContains: "different values", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.moduleSchema.Validate() + if tt.errContains == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } else { + if err == nil || !strings.Contains(err.Error(), tt.errContains) { + t.Fatalf("expected error to contain %q, got: %v", tt.errContains, err) + } + } + }) + } +} + +func TestModuleSchema_ValidateObjectUpdate(t *testing.T) { + tests := []struct { + name string + moduleSchema ModuleSchema + objectUpdate ObjectUpdate + errContains string + }{ + { + name: "valid object update", + moduleSchema: ModuleSchema{ + ObjectTypes: []ObjectType{ + { + Name: "object1", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + }, + }, + }, + }, + objectUpdate: ObjectUpdate{ + TypeName: "object1", + Key: "abc", + }, + errContains: "", + }, + { + name: "object type not found", + moduleSchema: ModuleSchema{ + ObjectTypes: []ObjectType{ + { + Name: "object1", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + }, + }, + }, + }, + objectUpdate: ObjectUpdate{ + TypeName: "object2", + Key: "abc", + }, + errContains: "object type \"object2\" not found in module schema", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.moduleSchema.ValidateObjectUpdate(tt.objectUpdate) + if tt.errContains == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } else { + if err == nil || !strings.Contains(err.Error(), tt.errContains) { + t.Fatalf("expected error to contain %q, got: %v", tt.errContains, err) + } + } + }) + } +} diff --git a/schema/name.go b/schema/name.go new file mode 100644 index 000000000000..fc384fec663b --- /dev/null +++ b/schema/name.go @@ -0,0 +1,15 @@ +package schema + +import "regexp" + +// NameFormat is the regular expression that a name must match. +// A name must start with a letter or underscore and can only contain letters, numbers, and underscores. +// A name must be at least one character long and can be at most 64 characters long. +const NameFormat = `^[a-zA-Z_][a-zA-Z0-9_]{0,63}$` + +var nameRegex = regexp.MustCompile(NameFormat) + +// ValidateName checks if the given name is a valid name conforming to NameFormat. +func ValidateName(name string) bool { + return nameRegex.MatchString(name) +} diff --git a/schema/name_test.go b/schema/name_test.go new file mode 100644 index 000000000000..2383b880db1a --- /dev/null +++ b/schema/name_test.go @@ -0,0 +1,31 @@ +package schema + +import "testing" + +func TestValidateName(t *testing.T) { + tests := []struct { + name string + valid bool + }{ + {"", false}, + {"a", true}, + {"A", true}, + {"_", true}, + {"abc123_def789", true}, + {"0", false}, + {"a0", true}, + {"a_", true}, + {"$a", false}, + {"a b", false}, + {"pretty_unnecessarily_long_but_valid_name", true}, + {"totally_unnecessarily_long_and_invalid_name_sdgkhwersdglkhweriqwery3258", false}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if ValidateName(test.name) != test.valid { + t.Errorf("expected %v for name %q", test.valid, test.name) + } + }) + } +} diff --git a/schema/object_type.go b/schema/object_type.go new file mode 100644 index 000000000000..31aaf71aa6be --- /dev/null +++ b/schema/object_type.go @@ -0,0 +1,87 @@ +package schema + +import "fmt" + +// ObjectType describes an object type a module schema. +type ObjectType struct { + // Name is the name of the object type. It must be unique within the module schema + // and conform to the NameFormat regular expression. + Name string + + // KeyFields is a list of fields that make up the primary key of the object. + // It can be empty in which case indexers should assume that this object is + // a singleton and only has one value. Field names must be unique within the + // object between both key and value fields. Key fields CANNOT be nullable. + KeyFields []Field + + // ValueFields is a list of fields that are not part of the primary key of the object. + // It can be empty in the case where all fields are part of the primary key. + // Field names must be unique within the object between both key and value fields. + ValueFields []Field + + // RetainDeletions is a flag that indicates whether the indexer should retain + // deleted rows in the database and flag them as deleted rather than actually + // deleting the row. For many types of data in state, the data is deleted even + // though it is still valid in order to save space. Indexers will want to have + // the option of retaining such data and distinguishing from other "true" deletions. + RetainDeletions bool + + UniqueConstraints []UniqueConstraint +} + +// Validate validates the object type. +func (o ObjectType) Validate() error { + if !ValidateName(o.Name) { + return fmt.Errorf("invalid object type name %q", o.Name) + } + + fieldNames := map[string]bool{} + for _, field := range o.KeyFields { + if err := field.Validate(); err != nil { + return fmt.Errorf("invalid key field %q: %w", field.Name, err) + } + + if field.Nullable { + return fmt.Errorf("key field %q cannot be nullable", field.Name) + } + + if fieldNames[field.Name] { + return fmt.Errorf("duplicate field name %q", field.Name) + } + fieldNames[field.Name] = true + } + + for _, field := range o.ValueFields { + if err := field.Validate(); err != nil { + return fmt.Errorf("invalid value field %q: %w", field.Name, err) + } + + if fieldNames[field.Name] { + return fmt.Errorf("duplicate field name %q", field.Name) + } + fieldNames[field.Name] = true + } + + if len(o.KeyFields) == 0 && len(o.ValueFields) == 0 { + return fmt.Errorf("object type %q has no key or value fields", o.Name) + } + + return nil +} + +// ValidateObjectUpdate validates that the update conforms to the object type. +func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error { + if o.Name != update.TypeName { + return fmt.Errorf("object type name %q does not match update type name %q", o.Name, update.TypeName) + } + + if err := ValidateForKeyFields(o.KeyFields, update.Key); err != nil { + return fmt.Errorf("invalid key for object type %q: %w", update.TypeName, err) + } + + if update.Delete { + return nil + } + + return ValidateForValueFields(o.ValueFields, update.Value) +} diff --git a/schema/object_type_test.go b/schema/object_type_test.go new file mode 100644 index 000000000000..e945239f2c67 --- /dev/null +++ b/schema/object_type_test.go @@ -0,0 +1,243 @@ +package schema + +import ( + "strings" + "testing" +) + +var object1Type = ObjectType{ + Name: "object1", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + }, +} + +var object2Type = ObjectType{ + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + { + Name: "field2", + Kind: Int32Kind, + }, + }, +} + +var object3Type = ObjectType{ + Name: "object3", + ValueFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + { + Name: "field2", + Kind: Int32Kind, + }, + }, +} + +var object4Type = ObjectType{ + Name: "object4", + KeyFields: []Field{ + { + Name: "field1", + Kind: Int32Kind, + }, + }, + ValueFields: []Field{ + { + Name: "field2", + Kind: StringKind, + }, + }, +} + +func TestObjectType_Validate(t *testing.T) { + tests := []struct { + name string + objectType ObjectType + errContains string + }{ + { + name: "valid object type", + objectType: object1Type, + errContains: "", + }, + { + name: "empty object type name", + objectType: ObjectType{ + Name: "", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + }, + }, + errContains: "invalid object type name", + }, + { + name: "invalid key field", + objectType: ObjectType{ + Name: "object1", + KeyFields: []Field{ + { + Name: "", + Kind: StringKind, + }, + }, + }, + errContains: "invalid field name", + }, + { + name: "invalid value field", + objectType: ObjectType{ + Name: "object1", + ValueFields: []Field{ + { + Kind: StringKind, + }, + }, + }, + errContains: "invalid field name", + }, + { + name: "no fields", + objectType: ObjectType{Name: "object0"}, + errContains: "has no key or value fields", + }, + { + name: "duplicate field", + objectType: ObjectType{ + Name: "object1", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + }, + ValueFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + }, + }, + errContains: "duplicate field name", + }, + { + name: "duplicate field 22", + objectType: ObjectType{ + Name: "object1", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + }, + { + Name: "field1", + Kind: StringKind, + }, + }, + }, + errContains: "duplicate field name", + }, + { + name: "nullable key field", + objectType: ObjectType{ + Name: "objectNullKey", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + Nullable: true, + }, + }, + }, + errContains: "key field \"field1\" cannot be nullable", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.objectType.Validate() + if tt.errContains == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } else { + if err == nil || !strings.Contains(err.Error(), tt.errContains) { + t.Fatalf("expected error to contain %q, got: %v", tt.errContains, err) + } + } + }) + } +} + +func TestObjectType_ValidateObjectUpdate(t *testing.T) { + tests := []struct { + name string + objectType ObjectType + object ObjectUpdate + errContains string + }{ + { + name: "wrong name", + objectType: object1Type, + object: ObjectUpdate{ + TypeName: "object2", + Key: "hello", + }, + errContains: "does not match update type name", + }, + { + name: "invalid value", + objectType: object1Type, + object: ObjectUpdate{ + TypeName: "object1", + Key: 123, + }, + errContains: "invalid value", + }, + { + name: "valid update", + objectType: object4Type, + object: ObjectUpdate{ + TypeName: "object4", + Key: int32(123), + Value: "hello", + }, + }, + { + name: "valid deletion", + objectType: object4Type, + object: ObjectUpdate{ + TypeName: "object4", + Key: int32(123), + Value: "ignored!", + Delete: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.objectType.ValidateObjectUpdate(tt.object) + if tt.errContains == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } else { + if err == nil || !strings.Contains(err.Error(), tt.errContains) { + t.Fatalf("expected error to contain %q, got: %v", tt.errContains, err) + } + } + }) + } +} diff --git a/indexer/base/object_update.go b/schema/object_update.go similarity index 82% rename from indexer/base/object_update.go rename to schema/object_update.go index e99d625404fb..455c4a850afb 100644 --- a/indexer/base/object_update.go +++ b/schema/object_update.go @@ -1,8 +1,9 @@ -package indexerbase +package schema + +import "sort" // ObjectUpdate represents an update operation on an object in a module's state. type ObjectUpdate struct { - // TypeName is the name of the object type in the module's schema. TypeName string @@ -33,10 +34,28 @@ type ObjectUpdate struct { // may not filter out fields that were unchanged. However, if a field is omitted from the update // it should be considered unchanged. type ValueUpdates interface { - // Iterate iterates over the fields and values in the object update. The function should return // true to continue iteration or false to stop iteration. Each field value should conform // to the requirements of that field's type in the schema. Iterate returns an error if // it was unable to decode the values properly (which could be the case in lazy evaluation). Iterate(func(col string, value interface{}) bool) error } + +// MapValueUpdates is a map-based implementation of ValueUpdates which always iterates +// over keys in sorted order. +type MapValueUpdates map[string]interface{} + +// Iterate implements the ValueUpdates interface. +func (m MapValueUpdates) Iterate(fn func(col string, value interface{}) bool) error { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + if !fn(k, m[k]) { + return nil + } + } + return nil +} diff --git a/schema/object_update_test.go b/schema/object_update_test.go new file mode 100644 index 000000000000..eb5a156f18de --- /dev/null +++ b/schema/object_update_test.go @@ -0,0 +1,52 @@ +package schema + +import "testing" + +func TestMapValueUpdates_Iterate(t *testing.T) { + updates := MapValueUpdates(map[string]interface{}{ + "a": "abc", + "b": 123, + }) + + got := map[string]interface{}{} + err := updates.Iterate(func(fieldname string, value interface{}) bool { + got[fieldname] = value + return true + }) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(got) != 2 { + t.Errorf("expected 2 updates, got: %v", got) + } + + if got["a"] != "abc" { + t.Errorf("expected a=abc, got: %v", got) + } + + if got["b"] != 123 { + t.Errorf("expected b=123, got: %v", got) + } + + got = map[string]interface{}{} + err = updates.Iterate(func(fieldname string, value interface{}) bool { + if len(got) == 1 { + return false + } + got[fieldname] = value + return true + }) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(got) != 1 { + t.Errorf("expected 1 updates, got: %v", got) + } + + // should have gotten the first field in order + if got["a"] != "abc" { + t.Errorf("expected a=abc, got: %v", got) + } +} diff --git a/indexer/testing/CHANGELOG.md b/schema/testing/CHANGELOG.md similarity index 100% rename from indexer/testing/CHANGELOG.md rename to schema/testing/CHANGELOG.md diff --git a/schema/testing/README.md b/schema/testing/README.md new file mode 100644 index 000000000000..5b89dcc2c685 --- /dev/null +++ b/schema/testing/README.md @@ -0,0 +1,4 @@ +# Schema Testing + +This module contains core test utilities and fixtures for testing schema and indexer functionality. It is managed as a separate go module to manage versions better and allow for dependencies on useful testing libraries without imposing those +elsewhere. \ No newline at end of file diff --git a/schema/testing/app.go b/schema/testing/app.go new file mode 100644 index 000000000000..56f50e03eab4 --- /dev/null +++ b/schema/testing/app.go @@ -0,0 +1,20 @@ +package schematesting + +import ( + "fmt" + + "pgregory.net/rapid" + + "cosmossdk.io/schema" +) + +var AppSchemaGen = rapid.Custom(func(t *rapid.T) map[string]schema.ModuleSchema { + schema := make(map[string]schema.ModuleSchema) + numModules := rapid.IntRange(1, 10).Draw(t, "numModules") + for i := 0; i < numModules; i++ { + moduleName := NameGen.Draw(t, "moduleName") + moduleSchema := ModuleSchemaGen.Draw(t, fmt.Sprintf("moduleSchema[%s]", moduleName)) + schema[moduleName] = moduleSchema + } + return schema +}) diff --git a/schema/testing/app_test.go b/schema/testing/app_test.go new file mode 100644 index 000000000000..ac2600e238f2 --- /dev/null +++ b/schema/testing/app_test.go @@ -0,0 +1,18 @@ +package schematesting + +import ( + "testing" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +func TestAppSchema(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + schema := AppSchemaGen.Draw(t, "schema") + for moduleName, moduleSchema := range schema { + require.NotEmpty(t, moduleName) + require.NoError(t, moduleSchema.Validate()) + } + }) +} diff --git a/schema/testing/appdata/app_data.go b/schema/testing/appdata/app_data.go new file mode 100644 index 000000000000..1c53f78716c9 --- /dev/null +++ b/schema/testing/appdata/app_data.go @@ -0,0 +1,131 @@ +package appdatatest + +import ( + "fmt" + + "pgregory.net/rapid" + + "cosmossdk.io/schema" + "cosmossdk.io/schema/appdata" + schematesting "cosmossdk.io/schema/testing" + "cosmossdk.io/schema/testing/statesim" +) + +type SimulatorOptions struct { + AppSchema map[string]schema.ModuleSchema + Listener appdata.Listener + EventAlignedWrites bool + StateSimOptions statesim.Options + StartBlockDataGen *rapid.Generator[appdata.StartBlockData] + TxDataGen *rapid.Generator[appdata.TxData] + EventDataGen *rapid.Generator[appdata.EventData] +} + +type Simulator struct { + state *statesim.App + options SimulatorOptions + blockNum uint64 + blockDataGen *rapid.Generator[BlockData] +} + +type BlockData = []appdata.Packet + +func NewSimulator(options SimulatorOptions) *Simulator { + if options.AppSchema == nil { + options.AppSchema = schematesting.ExampleAppSchema + } + + sim := &Simulator{ + state: statesim.NewApp(options.AppSchema, options.StateSimOptions), + options: options, + } + + return sim +} + +func (a *Simulator) Initialize() error { + if f := a.options.Listener.InitializeModuleData; f != nil { + err := a.state.ScanModuleSchemas(func(moduleName string, moduleSchema schema.ModuleSchema) error { + return f(appdata.ModuleInitializationData{ModuleName: moduleName, Schema: moduleSchema}) + }) + if err != nil { + return err + } + } + + return nil +} + +func (a *Simulator) BlockDataGen() *rapid.Generator[BlockData] { + return a.BlockDataGenN(100) +} + +func (a *Simulator) BlockDataGenN(maxUpdatesPerBlock int) *rapid.Generator[BlockData] { + numUpdatesGen := rapid.IntRange(1, maxUpdatesPerBlock) + + return rapid.Custom(func(t *rapid.T) BlockData { + var packets BlockData + + updateSet := map[string]bool{} + // filter out any updates to the same key from this block, otherwise we can end up with weird errors + updateGen := a.state.UpdateGen().Filter(func(data appdata.ObjectUpdateData) bool { + for _, update := range data.Updates { + _, existing := updateSet[fmt.Sprintf("%s:%v", data.ModuleName, update.Key)] + if existing { + return false + } + } + return true + }) + numUpdates := numUpdatesGen.Draw(t, "numUpdates") + for i := 0; i < numUpdates; i++ { + data := updateGen.Draw(t, fmt.Sprintf("update[%d]", i)) + for _, update := range data.Updates { + updateSet[fmt.Sprintf("%s:%v", data.ModuleName, update.Key)] = true + } + packets = append(packets, data) + } + + return packets + }) +} + +func (a *Simulator) ProcessBlockData(data BlockData) error { + a.blockNum++ + + if f := a.options.Listener.StartBlock; f != nil { + err := f(appdata.StartBlockData{Height: a.blockNum}) + if err != nil { + return err + } + } + + for _, packet := range data { + err := a.options.Listener.SendPacket(packet) + if err != nil { + return err + } + + if updateData, ok := packet.(appdata.ObjectUpdateData); ok { + for _, update := range updateData.Updates { + err = a.state.ApplyUpdate(updateData.ModuleName, update) + if err != nil { + return err + } + } + } + } + + if f := a.options.Listener.Commit; f != nil { + err := f(appdata.CommitData{}) + if err != nil { + return err + } + } + + return nil +} + +func (a *Simulator) AppState() *statesim.App { + return a.state +} diff --git a/schema/testing/appdata/app_data_test.go b/schema/testing/appdata/app_data_test.go new file mode 100644 index 000000000000..3a43e917a8e5 --- /dev/null +++ b/schema/testing/appdata/app_data_test.go @@ -0,0 +1,30 @@ +package appdatatest + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + "gotest.tools/v3/golden" + + "cosmossdk.io/schema/testing" +) + +func TestAppSimulator_ExampleSchema(t *testing.T) { + out := &bytes.Buffer{} + appSim := NewSimulator(SimulatorOptions{ + AppSchema: schematesting.ExampleAppSchema, + Listener: WriterListener(out), + }) + + require.NoError(t, appSim.Initialize()) + + blockDataGen := appSim.BlockDataGen() + + for i := 0; i < 10; i++ { + data := blockDataGen.Example(i + 1) + require.NoError(t, appSim.ProcessBlockData(data)) + } + + golden.Assert(t, out.String(), "app_sim_example_schema.txt") +} diff --git a/schema/testing/appdata/json.go b/schema/testing/appdata/json.go new file mode 100644 index 000000000000..379bfee8faf3 --- /dev/null +++ b/schema/testing/appdata/json.go @@ -0,0 +1,40 @@ +package appdatatest + +import ( + "pgregory.net/rapid" + + "cosmossdk.io/schema/appdata" +) + +func jsonValueGen() *rapid.Generator[any] { + return rapid.OneOf( + rapid.Bool().AsAny(), + rapid.Float64().AsAny(), + rapid.String().AsAny(), + rapid.MapOf(rapid.String(), rapid.Deferred(jsonValueGen)).AsAny(), + rapid.SliceOf(rapid.Deferred(jsonValueGen)).AsAny(), + ) +} + +var JSONValueGen = jsonValueGen() + +var JSONObjectGen = rapid.MapOf(rapid.String(), JSONValueGen) + +var JSONArrayGen = rapid.SliceOf(JSONValueGen) + +func JSONObjectWithKeys(keys ...string) *rapid.Generator[map[string]interface{}] { + return rapid.MapOf(rapid.SampledFrom(keys), JSONValueGen) +} + +func StringMapWithKeys(keys ...string) *rapid.Generator[map[string]string] { + return rapid.MapOf(rapid.SampledFrom(keys), rapid.String()) +} + +// events can consist of names separated by dots, e.g. "message.sent" +const eventTypeFormat = `^([a-zA-Z_][a-zA-Z0-9_]*\.)*[A-Za-z_][A-Za-z0-9_]$` + +var DefaultEventDataGen = rapid.Custom(func(t *rapid.T) appdata.EventData { + return appdata.EventData{ + Type: rapid.StringMatching(`^$`).Draw(t, "type"), + } +}) diff --git a/schema/testing/appdata/testdata/app_sim_example_schema.txt b/schema/testing/appdata/testdata/app_sim_example_schema.txt new file mode 100644 index 000000000000..95281780f917 --- /dev/null +++ b/schema/testing/appdata/testdata/app_sim_example_schema.txt @@ -0,0 +1,91 @@ +InitializeModuleSchema: all_kinds {"ObjectTypes":[{"Name":"test_string","KeyFields":[{"Name":"keyNotNull","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":1,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":1,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_bytes","KeyFields":[{"Name":"keyNotNull","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":2,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":2,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_int8","KeyFields":[{"Name":"keyNotNull","Kind":3,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":3,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":3,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":3,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_uint8","KeyFields":[{"Name":"keyNotNull","Kind":4,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":4,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":4,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":4,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_int16","KeyFields":[{"Name":"keyNotNull","Kind":5,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":5,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":5,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":5,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_uint16","KeyFields":[{"Name":"keyNotNull","Kind":6,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":6,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":6,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":6,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_int32","KeyFields":[{"Name":"keyNotNull","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":7,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":7,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_uint32","KeyFields":[{"Name":"keyNotNull","Kind":8,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":8,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":8,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":8,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_int64","KeyFields":[{"Name":"keyNotNull","Kind":9,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":9,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":9,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":9,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_uint64","KeyFields":[{"Name":"keyNotNull","Kind":10,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":10,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":10,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":10,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_integer","KeyFields":[{"Name":"keyNotNull","Kind":11,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":11,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":11,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":11,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_decimal","KeyFields":[{"Name":"keyNotNull","Kind":12,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":12,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":12,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":12,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_bool","KeyFields":[{"Name":"keyNotNull","Kind":13,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":13,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":13,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":13,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_time","KeyFields":[{"Name":"keyNotNull","Kind":14,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":14,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":14,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":14,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_duration","KeyFields":[{"Name":"keyNotNull","Kind":15,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":15,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":15,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":15,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_float32","KeyFields":[{"Name":"keyNotNull","Kind":16,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":16,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":16,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":16,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_float64","KeyFields":[{"Name":"keyNotNull","Kind":17,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":17,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":17,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":17,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_bech32address","KeyFields":[{"Name":"keyNotNull","Kind":18,"Nullable":false,"AddressPrefix":"cosmos","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":18,"Nullable":true,"AddressPrefix":"cosmos","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":18,"Nullable":false,"AddressPrefix":"cosmos","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":18,"Nullable":true,"AddressPrefix":"cosmos","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_enum","KeyFields":[{"Name":"keyNotNull","Kind":19,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"test_enum","Values":["foo","bar","baz"]}},{"Name":"keyNullable","Kind":19,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"test_enum","Values":["foo","bar","baz"]}}],"ValueFields":[{"Name":"valNotNull","Kind":19,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"test_enum","Values":["foo","bar","baz"]}},{"Name":"valNullable","Kind":19,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"test_enum","Values":["foo","bar","baz"]}}],"RetainDeletions":false}]} +InitializeModuleSchema: test_cases {"ObjectTypes":[{"Name":"Singleton","KeyFields":[],"ValueFields":[{"Name":"Value","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value2","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"Simple","KeyFields":[{"Name":"Key","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"Value1","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value2","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"Two Keys","KeyFields":[{"Name":"Key1","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Key2","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":null,"RetainDeletions":false},{"Name":"Three Keys","KeyFields":[{"Name":"Key1","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Key2","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Key3","Kind":10,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"Value1","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"Many Values","KeyFields":[{"Name":"Key","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"Value1","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value2","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value3","Kind":17,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value4","Kind":10,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"RetainDeletions","KeyFields":[{"Name":"Key","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"Value1","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value2","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":true}]} +Initialize: {false} +StartBlock: 1 +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"a⃝?\rA�֍","Value":{"Value1":-2147483648,"Value2":"5j6jAR12ASgIHQMBA54DACYFGgI=","Value3":1817374971955183.2},"Delete":false} +Commit +StartBlock: 2 +OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["APo3CwC1wwTmagHIAorBdQoBAQMA0woAITL/GQH6tkT/AQE=",null],"Value":["/Mt59F4hjgGqAQ0B/wWlAqn2UqEAAQIAAU8MAYoCDzs3AgJoAAE/FgNLwAnnDcIeAAUYNS1Ytg==","ActiKPrSHCMDEjMMlg04AFflAz0qxAaWAASFAwkAywIGBxQAzF8xJQ=="],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["HDUI+gAHAhpSCu4LYgAB1wMAtQAEBctXUQ==",null],"Value":{"valNullable":"BwERCwQtBAMI8Br2Awafjke1kAEDAQMA//ssKgTpMgYMAAMBB+oUGAACBBoALwcePkwA//8pHQCtTvylLIXt8A=="},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"a⃝?\rA�֍","Value":{"Value1":197503,"Value4":4093396},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_duration","Key":[-322686,-3],"Value":[4067136,null],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_int8","Key":[6,0],"Value":{"valNotNull":-59},"Delete":false} +Commit +StartBlock: 3 +OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[true,null],"Value":{},"Delete":false} +Commit +StartBlock: 4 +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"a⃝?\rA�֍","Value":{"Value1":-1,"Value2":"3CsGAls=","Value4":58783},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_string","Key":["A^'","*?{a"],"Value":{"valNullable":"‮⋁"},"Delete":false} +Commit +StartBlock: 5 +OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["APo3CwC1wwTmagHIAorBdQoBAQMA0woAITL/GQH6tkT/AQE=",null],"Value":{"valNotNull":"OgOQG+lGAgF5Y+EAAwAAAkQHATYDYXUB","valNullable":"AQGXARQPGgE7AgHIDYoAACEBC/6MyDYLuADecgDPACvqAAELpGkACwAAqOIJAgEDKw=="},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"","Value":[-8564477,"AcQ="],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_duration","Key":[-1,null],"Value":{},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"\t𓐸\u0026A","Value":[-11084,"",5.3864324145915995e+81,285497682],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"\u0026A","Value":{"Value2":"Ab/p"},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":{},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Two Keys","Key":["؁〩a_𞥟",-120667635],"Value":null,"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[false,null],"Value":[true,null],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_int8","Key":[-4,6],"Value":{},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Singleton","Key":null,"Value":{},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"\u0026`aᾼa\u0000#ᛮ𞥞aᾮ\u0026₰?𐧀~ ा၉𩮘A` ","Value":{"Value1":163,"Value2":"iwGmHAAWaAIPz6U="},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"𝞒𐄍#?ᾫ","Value":{"Value1":246341895},"Delete":false} +Commit +StartBlock: 6 +OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"\u0026`aᾼa\u0000#ᛮ𞥞aᾮ\u0026₰?𐧀~ ा၉𩮘A` ","Value":null,"Delete":true} +OnObjectUpdate: all_kinds: {"TypeName":"test_enum","Key":["bar","foo"],"Value":["bar",null],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_enum","Key":["foo","bar"],"Value":{},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bytes","Key":["BAAZ/Mo=","C7U="],"Value":{},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"Ƶa\u0026[.\u0026a.","Value":[-314,"AA0LT19MujQyf/8FbQMDawAM"],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_enum","Key":["bar","foo"],"Value":{"valNullable":"foo"},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[true,null],"Value":{"valNotNull":true,"valNullable":null},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"_$A\r","Value":{"Value1":1319133},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"","Value":[-14,"BA=="],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"Ƶa\u0026[.\u0026a.","Value":null,"Delete":true} +OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"g$🄛\u0026#?","Value":{"Value1":2,"Value2":"BvIE"},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Singleton","Key":null,"Value":null,"Delete":true} +Commit +StartBlock: 7 +OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"","Value":[-4,"nAABEGi4pg=="],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"aᱻꞘ","Value":[-11,"CA=="],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":{"Value1":-61,"Value4":4},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"+","Value":[-1891215,"+UwDKRIC"],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["NAHCtAMbAQEHTAI3DgcfG8hLASMq4DICAj41AAubARsBATQg3gCppQMAAQwHASEDAwD+55ELSKjFAC4oGwgKBA==","nyYBB4AGAJnxe792TCHxAg5qHS4A"],"Value":["AG2pEQEGAQFzIAFWAQEACv8CAgECgP8AtA==",null],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Two Keys","Key":["!฿*$\u0026AȺ#˼%\u0000ⅷ_ŕ,A",-467],"Value":null,"Delete":false} +Commit +StartBlock: 8 +OnObjectUpdate: all_kinds: {"TypeName":"test_float32","Key":[-11851129000000000000,-2014.5674],"Value":[-1.0672615e-18,2.771192],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"\u0026A","Value":[3901734,"AwMF"],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"Aः𒑨Dz؅","Value":[0,""],"Delete":false} +Commit +StartBlock: 9 +OnObjectUpdate: all_kinds: {"TypeName":"test_int64","Key":[-4,5],"Value":[100,null],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bytes","Key":["/w==",null],"Value":["IgACFRoAfgMDBMi/",null],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[true,null],"Value":null,"Delete":true} +OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"","Value":{"Value2":"BgEHAAUG0QgDAw=="},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["HDUI+gAHAhpSCu4LYgAB1wMAtQAEBctXUQ==",null],"Value":null,"Delete":true} +OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[false,null],"Value":[true,null],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bytes","Key":["/w==",null],"Value":["NQE=",null],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"\t𓐸\u0026A","Value":null,"Delete":true} +OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[false,true],"Value":[false,null],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":{"Value3":5984325148376916},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_uint16","Key":[0,null],"Value":[43,0],"Delete":false} +Commit +StartBlock: 10 +OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"\\༸՚@","Value":[6,"AA=="],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_decimal","Key":["31940815527640952","2665097019"],"Value":["930126","5301e322"],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_int16","Key":[-8610,null],"Value":[1195,-25804],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[true,null],"Value":[false,true],"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":[-1,"AwFIAwmRCA==",5.463114807757741e-9,2],"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[false,false],"Value":{"valNotNull":true},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_decimal","Key":["82509790016910",null],"Value":{"valNotNull":"-2","valNullable":null},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Singleton","Key":null,"Value":{},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_int64","Key":[-4,5],"Value":null,"Delete":true} +OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["NAHCtAMbAQEHTAI3DgcfG8hLASMq4DICAj41AAubARsBATQg3gCppQMAAQwHASEDAwD+55ELSKjFAC4oGwgKBA==","nyYBB4AGAJnxe792TCHxAg5qHS4A"],"Value":null,"Delete":true} +OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["ZPpWX+smAAJmAKAc//8KTwAuXQAUHKICBwEBFFB6Bx8IVh0kugBLAVSVFhYDtDULkwIwAYwA+gfMA6k=",null],"Value":{},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":{"Value1":847967},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_decimal","Key":["50.676",null],"Value":{"valNotNull":"-11515688332E35"},"Delete":false} +OnObjectUpdate: all_kinds: {"TypeName":"test_enum","Key":["bar","foo"],"Value":{"valNotNull":"bar"},"Delete":false} +OnObjectUpdate: test_cases: {"TypeName":"Two Keys","Key":["!฿*$\u0026AȺ#˼%\u0000ⅷ_ŕ,A",-467],"Value":null,"Delete":false} +Commit diff --git a/schema/testing/appdata/write_listener.go b/schema/testing/appdata/write_listener.go new file mode 100644 index 000000000000..41a1dcb24043 --- /dev/null +++ b/schema/testing/appdata/write_listener.go @@ -0,0 +1,41 @@ +package appdatatest + +import ( + "encoding/json" + "fmt" + "io" + + "cosmossdk.io/schema/appdata" +) + +func WriterListener(w io.Writer) appdata.Listener { + return appdata.Listener{ + StartBlock: func(data appdata.StartBlockData) error { + _, err := fmt.Fprintf(w, "StartBlock: %v\n", data) + return err + }, + OnTx: nil, + OnEvent: nil, + OnKVPair: nil, + Commit: func(data appdata.CommitData) error { + _, err := fmt.Fprintf(w, "Commit: %v\n", data) + return err + }, + InitializeModuleData: func(data appdata.ModuleInitializationData) error { + bz, err := json.Marshal(data) + if err != nil { + return err + } + _, err = fmt.Fprintf(w, "InitializeModuleData: %s\n", bz) + return err + }, + OnObjectUpdate: func(data appdata.ObjectUpdateData) error { + bz, err := json.Marshal(data) + if err != nil { + return err + } + _, err = fmt.Fprintf(w, "OnObjectUpdate: %s\n", bz) + return err + }, + } +} diff --git a/schema/testing/enum.go b/schema/testing/enum.go new file mode 100644 index 000000000000..f4aee77c9dba --- /dev/null +++ b/schema/testing/enum.go @@ -0,0 +1,18 @@ +package schematesting + +import ( + "pgregory.net/rapid" + + "cosmossdk.io/schema" +) + +var enumValuesGen = rapid.SliceOfNDistinct(NameGen, 1, 10, func(x string) string { return x }) + +var EnumDefinitionGen = rapid.Custom(func(t *rapid.T) schema.EnumDefinition { + enum := schema.EnumDefinition{ + Name: NameGen.Draw(t, "name"), + Values: enumValuesGen.Draw(t, "values"), + } + + return enum +}) diff --git a/schema/testing/enum_test.go b/schema/testing/enum_test.go new file mode 100644 index 000000000000..10d87db9e65d --- /dev/null +++ b/schema/testing/enum_test.go @@ -0,0 +1,15 @@ +package schematesting + +import ( + "testing" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +func TestEnumDefinition(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + enumDefinition := EnumDefinitionGen.Draw(t, "enum") + require.NoError(t, enumDefinition.Validate()) + }) +} diff --git a/schema/testing/example_schema.go b/schema/testing/example_schema.go new file mode 100644 index 000000000000..77d1e66c6879 --- /dev/null +++ b/schema/testing/example_schema.go @@ -0,0 +1,198 @@ +package schematesting + +import ( + "fmt" + + "cosmossdk.io/schema" +) + +var ExampleAppSchema = map[string]schema.ModuleSchema{ + "all_kinds": mkAllKindsModule(), + "test_cases": { + ObjectTypes: []schema.ObjectType{ + { + Name: "Singleton", + KeyFields: []schema.Field{}, + ValueFields: []schema.Field{ + { + Name: "Value", + Kind: schema.StringKind, + }, + { + Name: "Value2", + Kind: schema.BytesKind, + }, + }, + }, + { + Name: "Simple", + KeyFields: []schema.Field{ + { + Name: "Key", + Kind: schema.StringKind, + }, + }, + ValueFields: []schema.Field{ + { + Name: "Value1", + Kind: schema.Int32Kind, + }, + { + Name: "Value2", + Kind: schema.BytesKind, + }, + }, + }, + { + Name: "TwoKeys", + KeyFields: []schema.Field{ + { + Name: "Key1", + Kind: schema.StringKind, + }, + { + Name: "Key2", + Kind: schema.Int32Kind, + }, + }, + }, + { + Name: "ThreeKeys", + KeyFields: []schema.Field{ + { + Name: "Key1", + Kind: schema.StringKind, + }, + { + Name: "Key2", + Kind: schema.Int32Kind, + }, + { + Name: "Key3", + Kind: schema.Uint64Kind, + }, + }, + ValueFields: []schema.Field{ + { + Name: "Value1", + Kind: schema.Int32Kind, + }, + }, + }, + { + Name: "ManyValues", + KeyFields: []schema.Field{ + { + Name: "Key", + Kind: schema.StringKind, + }, + }, + ValueFields: []schema.Field{ + { + Name: "Value1", + Kind: schema.Int32Kind, + }, + { + Name: "Value2", + Kind: schema.BytesKind, + }, + { + Name: "Value3", + Kind: schema.Float64Kind, + }, + { + Name: "Value4", + Kind: schema.Uint64Kind, + }, + }, + }, + { + Name: "RetainDeletions", + KeyFields: []schema.Field{ + { + Name: "Key", + Kind: schema.StringKind, + }, + }, + ValueFields: []schema.Field{ + { + Name: "Value1", + Kind: schema.Int32Kind, + }, + { + Name: "Value2", + Kind: schema.BytesKind, + }, + }, + RetainDeletions: true, + }, + { + Name: "UniqueConstraint", + KeyFields: []schema.Field{ + { + Name: "Key", + Kind: schema.StringKind, + }, + }, + ValueFields: []schema.Field{ + { + Name: "Value1", + Kind: schema.Int32Kind, + }, + { + Name: "Value2", + Kind: schema.BytesKind, + }, + }, + UniqueConstraints: []schema.UniqueConstraint{ + {[]string{"Value1"}}, + }, + }, + }, + }, +} + +func mkAllKindsModule() schema.ModuleSchema { + mod := schema.ModuleSchema{} + + for i := 1; i < int(schema.MAX_VALID_KIND); i++ { + kind := schema.Kind(i) + typ := mkTestObjectType(kind) + mod.ObjectTypes = append(mod.ObjectTypes, typ) + } + + return mod +} + +func mkTestObjectType(kind schema.Kind) schema.ObjectType { + field := schema.Field{ + Kind: kind, + } + + if kind == schema.EnumKind { + field.EnumDefinition = testEnum + } + + if kind == schema.Bech32AddressKind { + field.AddressPrefix = "cosmos" + } + + keyField := field + keyField.Name = "key" + val1Field := field + val1Field.Name = "valNotNull" + val2Field := field + val2Field.Name = "valNullable" + val2Field.Nullable = true + + return schema.ObjectType{ + Name: fmt.Sprintf("test_%v", kind), + KeyFields: []schema.Field{keyField}, + ValueFields: []schema.Field{val1Field, val2Field}, + } +} + +var testEnum = schema.EnumDefinition{ + Name: "test_enum_type", + Values: []string{"foo", "bar", "baz"}, +} diff --git a/schema/testing/field.go b/schema/testing/field.go new file mode 100644 index 000000000000..8803e4b3150f --- /dev/null +++ b/schema/testing/field.go @@ -0,0 +1,168 @@ +package schematesting + +import ( + "fmt" + "time" + + "pgregory.net/rapid" + + "cosmossdk.io/schema" +) + +var ( + kindGen = rapid.Map(rapid.IntRange(int(schema.InvalidKind+1), int(schema.MAX_VALID_KIND-1)), + func(i int) schema.Kind { + return schema.Kind(i) + }) + boolGen = rapid.Bool() +) + +var FieldGen = rapid.Custom(func(t *rapid.T) schema.Field { + kind := kindGen.Draw(t, "kind") + field := schema.Field{ + Name: NameGen.Draw(t, "name"), + Kind: kind, + Nullable: boolGen.Draw(t, "nullable"), + } + + switch kind { + case schema.EnumKind: + field.EnumDefinition = EnumDefinitionGen.Draw(t, "enumDefinition") + case schema.Bech32AddressKind: + field.AddressPrefix = NameGen.Draw(t, "addressPrefix") + default: + } + + return field +}) + +func FieldValueGen(field schema.Field) *rapid.Generator[any] { + gen := baseFieldValue(field) + + if field.Nullable { + return rapid.OneOf(gen, rapid.Just[any](nil)).AsAny() + } + + return gen +} + +func baseFieldValue(field schema.Field) *rapid.Generator[any] { + switch field.Kind { + case schema.StringKind: + return rapid.StringOf(rapid.Rune().Filter(func(r rune) bool { + return r != 0 // filter out NULL characters + })).AsAny() + case schema.BytesKind: + return rapid.SliceOf(rapid.Byte()).AsAny() + case schema.Int8Kind: + return rapid.Int8().AsAny() + case schema.Int16Kind: + return rapid.Int16().AsAny() + case schema.Uint8Kind: + return rapid.Uint8().AsAny() + case schema.Uint16Kind: + return rapid.Uint16().AsAny() + case schema.Int32Kind: + return rapid.Int32().AsAny() + case schema.Uint32Kind: + return rapid.Uint32().AsAny() + case schema.Int64Kind: + return rapid.Int64().AsAny() + case schema.Uint64Kind: + return rapid.Uint64().AsAny() + case schema.Float32Kind: + return rapid.Float32().AsAny() + case schema.Float64Kind: + return rapid.Float64().AsAny() + case schema.IntegerStringKind: + return rapid.StringMatching(schema.IntegerFormat).AsAny() + case schema.DecimalStringKind: + return rapid.StringMatching(schema.DecimalFormat).AsAny() + case schema.BoolKind: + return rapid.Bool().AsAny() + case schema.TimeKind: + return rapid.Map(rapid.Int64(), func(i int64) time.Time { + return time.Unix(0, i) + }).AsAny() + case schema.DurationKind: + return rapid.Map(rapid.Int64(), func(i int64) time.Duration { + return time.Duration(i) + }).AsAny() + case schema.Bech32AddressKind: + return rapid.SliceOfN(rapid.Byte(), 20, 64).AsAny() + case schema.EnumKind: + return rapid.SampledFrom(field.EnumDefinition.Values).AsAny() + default: + panic(fmt.Errorf("unexpected kind: %v", field.Kind)) + } +} + +func KeyFieldsValueGen(keyFields []schema.Field) *rapid.Generator[any] { + if len(keyFields) == 0 { + return rapid.Just[any](nil) + } + + if len(keyFields) == 1 { + return FieldValueGen(keyFields[0]) + } + + gens := make([]*rapid.Generator[any], len(keyFields)) + for i, field := range keyFields { + gens[i] = FieldValueGen(field) + } + + return rapid.Custom(func(t *rapid.T) any { + values := make([]any, len(keyFields)) + for i, gen := range gens { + values[i] = gen.Draw(t, keyFields[i].Name) + } + return values + }) +} + +func ValueFieldsValueGen(valueFields []schema.Field, forUpdate bool) *rapid.Generator[any] { + // special case where there are no value fields + // we shouldn't end up here, but just in case + if len(valueFields) == 0 { + return rapid.Just[any](nil) + } + + gens := make([]*rapid.Generator[any], len(valueFields)) + for i, field := range valueFields { + gens[i] = FieldValueGen(field) + } + return rapid.Custom(func(t *rapid.T) any { + // return ValueUpdates 50% of the time + if boolGen.Draw(t, "valueUpdates") { + updates := map[string]any{} + + n := len(valueFields) + for i, gen := range gens { + lastField := i == n-1 + haveUpdates := len(updates) > 0 + // skip 50% of the time if this is an update + // but check if we have updates by the time we reach the last field + // so we don't have an empty update + if forUpdate && + (!lastField || haveUpdates) && + boolGen.Draw(t, fmt.Sprintf("skip_%s", valueFields[i].Name)) { + continue + } + updates[valueFields[i].Name] = gen.Draw(t, valueFields[i].Name) + } + + return schema.MapValueUpdates(updates) + } else { + if len(valueFields) == 1 { + return gens[0].Draw(t, valueFields[0].Name) + } + + values := make([]any, len(valueFields)) + for i, gen := range gens { + values[i] = gen.Draw(t, valueFields[i].Name) + } + + return values + } + }) +} diff --git a/schema/testing/field_test.go b/schema/testing/field_test.go new file mode 100644 index 000000000000..2c78ff906372 --- /dev/null +++ b/schema/testing/field_test.go @@ -0,0 +1,37 @@ +package schematesting + +import ( + "testing" + "unicode/utf8" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +func TestField(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + field := FieldGen.Draw(t, "field") + require.NoError(t, field.Validate()) + }) +} + +func TestFieldValue(t *testing.T) { + rapid.Check(t, checkFieldValue) +} + +func FuzzFieldValue(f *testing.F) { + strGen := rapid.String() + f.Fuzz(rapid.MakeFuzz(func(t *rapid.T) { + str := strGen.Draw(t, "str") + if !utf8.ValidString(str) { + t.Fatalf("invalid utf8 string: %q", str) + } + })) +} + +var checkFieldValue = func(t *rapid.T) { + field := FieldGen.Draw(t, "field") + require.NoError(t, field.Validate()) + fieldValue := FieldValueGen(field).Draw(t, "fieldValue") + require.NoError(t, field.ValidateValue(fieldValue)) +} diff --git a/schema/testing/go.mod b/schema/testing/go.mod new file mode 100644 index 000000000000..e1cfc343bcdf --- /dev/null +++ b/schema/testing/go.mod @@ -0,0 +1,20 @@ +module cosmossdk.io/schema/testing + +require ( + cosmossdk.io/schema v0.0.0 + github.com/stretchr/testify v1.9.0 + github.com/tidwall/btree v1.7.0 + gotest.tools/v3 v3.5.1 + pgregory.net/rapid v1.1.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace cosmossdk.io/schema => ./.. + +go 1.22 diff --git a/schema/testing/go.sum b/schema/testing/go.sum new file mode 100644 index 000000000000..393b537c2d4a --- /dev/null +++ b/schema/testing/go.sum @@ -0,0 +1,18 @@ +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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/schema/testing/module_schema.go b/schema/testing/module_schema.go new file mode 100644 index 000000000000..ee36232acf2f --- /dev/null +++ b/schema/testing/module_schema.go @@ -0,0 +1,31 @@ +package schematesting + +import ( + "fmt" + + "pgregory.net/rapid" + + "cosmossdk.io/schema" +) + +var ModuleSchemaGen = rapid.Custom(func(t *rapid.T) schema.ModuleSchema { + schema := schema.ModuleSchema{} + numObjectTypes := rapid.IntRange(1, 10).Draw(t, "numObjectTypes") + for i := 0; i < numObjectTypes; i++ { + objectType := ObjectTypeGen.Draw(t, fmt.Sprintf("objectType[%d]", i)) + schema.ObjectTypes = append(schema.ObjectTypes, objectType) + } + return schema +}).Filter(func(schema schema.ModuleSchema) bool { + // filter out enums with duplicate names + enumTypeNames := map[string]bool{} + for _, objectType := range schema.ObjectTypes { + if !checkDuplicateEnumName(enumTypeNames, objectType.KeyFields) { + return false + } + if !checkDuplicateEnumName(enumTypeNames, objectType.ValueFields) { + return false + } + } + return true +}) diff --git a/schema/testing/module_schema_test.go b/schema/testing/module_schema_test.go new file mode 100644 index 000000000000..91196d59aa18 --- /dev/null +++ b/schema/testing/module_schema_test.go @@ -0,0 +1,15 @@ +package schematesting + +import ( + "testing" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +func TestModuleSchema(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + schema := ModuleSchemaGen.Draw(t, "schema") + require.NoError(t, schema.Validate()) + }) +} diff --git a/schema/testing/name.go b/schema/testing/name.go new file mode 100644 index 000000000000..89d21faccec8 --- /dev/null +++ b/schema/testing/name.go @@ -0,0 +1,9 @@ +package schematesting + +import ( + "pgregory.net/rapid" + + "cosmossdk.io/schema" +) + +var NameGen = rapid.StringMatching(schema.NameFormat) diff --git a/schema/testing/name_test.go b/schema/testing/name_test.go new file mode 100644 index 000000000000..b4d9a44ea612 --- /dev/null +++ b/schema/testing/name_test.go @@ -0,0 +1,17 @@ +package schematesting + +import ( + "testing" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" + + "cosmossdk.io/schema" +) + +func TestName(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + name := NameGen.Draw(t, "name") + require.True(t, schema.ValidateName(name)) + }) +} diff --git a/schema/testing/object.go b/schema/testing/object.go new file mode 100644 index 000000000000..09e77052d0dd --- /dev/null +++ b/schema/testing/object.go @@ -0,0 +1,122 @@ +package schematesting + +import ( + "github.com/tidwall/btree" + "pgregory.net/rapid" + + "cosmossdk.io/schema" +) + +var fieldsGen = rapid.SliceOfNDistinct(FieldGen, 1, 12, func(f schema.Field) string { + return f.Name +}) + +var ObjectTypeGen = rapid.Custom(func(t *rapid.T) schema.ObjectType { + typ := schema.ObjectType{ + Name: NameGen.Draw(t, "name"), + } + + fields := fieldsGen.Draw(t, "fields") + numKeyFields := rapid.IntRange(0, len(fields)).Draw(t, "numKeyFields") + + typ.KeyFields = fields[:numKeyFields] + + for i := range typ.KeyFields { + // key fields can't be nullable + typ.KeyFields[i].Nullable = false + } + + typ.ValueFields = fields[numKeyFields:] + + typ.RetainDeletions = boolGen.Draw(t, "retainDeletions") + + return typ +}).Filter(func(typ schema.ObjectType) bool { + // filter out duplicate enum names + enumTypeNames := map[string]bool{} + if !checkDuplicateEnumName(enumTypeNames, typ.KeyFields) { + return false + } + if !checkDuplicateEnumName(enumTypeNames, typ.ValueFields) { + return false + } + return true +}) + +func checkDuplicateEnumName(enumTypeNames map[string]bool, fields []schema.Field) bool { + for _, field := range fields { + if field.Kind != schema.EnumKind { + continue + } + + if _, ok := enumTypeNames[field.EnumDefinition.Name]; ok { + return false + } + + enumTypeNames[field.EnumDefinition.Name] = true + } + return true +} + +func ObjectInsertGen(objectType schema.ObjectType) *rapid.Generator[schema.ObjectUpdate] { + return ObjectUpdateGen(objectType, nil) +} + +func ObjectUpdateGen(objectType schema.ObjectType, state *btree.Map[string, schema.ObjectUpdate]) *rapid.Generator[schema.ObjectUpdate] { + keyGen := KeyFieldsValueGen(objectType.KeyFields) + + if len(objectType.ValueFields) == 0 { + // special case where there are no value fields, + // so we just insert or delete, no updates + return rapid.Custom(func(t *rapid.T) schema.ObjectUpdate { + update := schema.ObjectUpdate{ + TypeName: objectType.Name, + } + + // 50% of the time delete existing key (when there are keys) + n := 0 + if state != nil { + n = state.Len() + } + if n > 0 && boolGen.Draw(t, "delete") { + i := rapid.IntRange(0, n-1).Draw(t, "index") + update.Key = state.Values()[i].Key + update.Delete = true + } else { + update.Key = keyGen.Draw(t, "key") + } + + return update + }) + } else { + insertValueGen := ValueFieldsValueGen(objectType.ValueFields, false) + updateValueGen := ValueFieldsValueGen(objectType.ValueFields, true) + return rapid.Custom(func(t *rapid.T) schema.ObjectUpdate { + update := schema.ObjectUpdate{ + TypeName: objectType.Name, + } + + // 50% of the time use existing key (when there are keys) + n := 0 + if state != nil { + n = state.Len() + } + if n > 0 && boolGen.Draw(t, "existingKey") { + i := rapid.IntRange(0, n-1).Draw(t, "index") + update.Key = state.Values()[i].Key + + // delete 50% of the time + if boolGen.Draw(t, "delete") { + update.Delete = true + } else { + update.Value = updateValueGen.Draw(t, "value") + } + } else { + update.Key = keyGen.Draw(t, "key") + update.Value = insertValueGen.Draw(t, "value") + } + + return update + }) + } +} diff --git a/schema/testing/object_test.go b/schema/testing/object_test.go new file mode 100644 index 000000000000..629d8e4764b0 --- /dev/null +++ b/schema/testing/object_test.go @@ -0,0 +1,31 @@ +package schematesting + +import ( + "testing" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +func TestObject(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + objectType := ObjectTypeGen.Draw(t, "object") + require.NoError(t, objectType.Validate()) + }) +} + +func TestObjectUpdate(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + objectType := ObjectTypeGen.Draw(t, "object") + require.NoError(t, objectType.Validate()) + update := ObjectInsertGen(objectType).Draw(t, "update") + require.NoError(t, objectType.ValidateObjectUpdate(update)) + }) +} + +func TestExample(t *testing.T) { + objectType := ObjectTypeGen.Example(1) + update := ObjectInsertGen(objectType).Example(2) + t.Logf("objectType: %+v", objectType) + t.Logf("update: %+v", update) +} diff --git a/schema/testing/statesim/app.go b/schema/testing/statesim/app.go new file mode 100644 index 000000000000..b242429e73f0 --- /dev/null +++ b/schema/testing/statesim/app.go @@ -0,0 +1,103 @@ +package statesim + +import ( + "fmt" + + "github.com/stretchr/testify/require" + "github.com/tidwall/btree" + "pgregory.net/rapid" + + "cosmossdk.io/schema" + "cosmossdk.io/schema/appdata" +) + +type App struct { + moduleStates *btree.Map[string, *Module] + updateGen *rapid.Generator[appdata.ObjectUpdateData] +} + +func NewApp(appSchema map[string]schema.ModuleSchema, options Options) *App { + moduleStates := &btree.Map[string, *Module]{} + var moduleNames []string + + for moduleName, moduleSchema := range appSchema { + moduleState := NewModule(moduleSchema, options) + moduleStates.Set(moduleName, moduleState) + moduleNames = append(moduleNames, moduleName) + } + + moduleNameSelector := rapid.Map(rapid.IntRange(0, len(moduleNames)), func(u int) string { + return moduleNames[u] + }) + + numUpdatesGen := rapid.IntRange(1, 2) + updateGen := rapid.Custom(func(t *rapid.T) appdata.ObjectUpdateData { + moduleName := moduleNameSelector.Draw(t, "moduleName") + moduleState, ok := moduleStates.Get(moduleName) + require.True(t, ok) + numUpdates := numUpdatesGen.Draw(t, "numUpdates") + updates := make([]schema.ObjectUpdate, numUpdates) + for i := 0; i < numUpdates; i++ { + update := moduleState.UpdateGen().Draw(t, fmt.Sprintf("update[%d]", i)) + updates[i] = update + } + return appdata.ObjectUpdateData{ + ModuleName: moduleName, + Updates: updates, + } + }) + + return &App{ + moduleStates: moduleStates, + updateGen: updateGen, + } +} + +func (a *App) ApplyUpdate(moduleName string, update schema.ObjectUpdate) error { + moduleState, ok := a.moduleStates.Get(moduleName) + if !ok { + return fmt.Errorf("module %s not found", moduleName) + } + + return moduleState.ApplyUpdate(update) +} + +func (a *App) UpdateGen() *rapid.Generator[appdata.ObjectUpdateData] { + return a.updateGen +} + +func (a *App) ScanModuleSchemas(f func(string, schema.ModuleSchema) error) error { + var err error + a.moduleStates.Scan(func(key string, value *Module) bool { + err = f(key, value.moduleSchema) + return err == nil + }) + return err +} + +func (a *App) GetModule(moduleName string) (*Module, bool) { + return a.moduleStates.Get(moduleName) +} + +func (a *App) ScanState(f func(moduleName string, update schema.ObjectUpdate) error) error { + var err error + a.moduleStates.Scan(func(moduleName string, value *Module) bool { + err = value.ScanState(func(update schema.ObjectUpdate) error { + return f(moduleName, update) + }) + return err == nil + }) + return err +} + +func (a *App) ScanObjectCollections(f func(moduleName string, collection *ObjectCollection) error) error { + var err error + a.moduleStates.Scan(func(moduleName string, value *Module) bool { + value.objectCollections.Scan(func(key string, value *ObjectCollection) bool { + err = f(moduleName, value) + return err == nil + }) + return err == nil + }) + return err +} diff --git a/schema/testing/statesim/module.go b/schema/testing/statesim/module.go new file mode 100644 index 000000000000..15c2f663ee69 --- /dev/null +++ b/schema/testing/statesim/module.go @@ -0,0 +1,72 @@ +package statesim + +import ( + "fmt" + + "github.com/stretchr/testify/require" + "github.com/tidwall/btree" + "pgregory.net/rapid" + + "cosmossdk.io/schema" +) + +type Module struct { + moduleSchema schema.ModuleSchema + objectCollections *btree.Map[string, *ObjectCollection] + updateGen *rapid.Generator[schema.ObjectUpdate] +} + +func NewModule(moduleSchema schema.ModuleSchema, options Options) *Module { + objectCollections := &btree.Map[string, *ObjectCollection]{} + var objectTypeNames []string + for _, objectType := range moduleSchema.ObjectTypes { + objectCollection := NewObjectCollection(objectType, options) + objectCollections.Set(objectType.Name, objectCollection) + objectTypeNames = append(objectTypeNames, objectType.Name) + } + + objectTypeSelector := rapid.Map(rapid.IntRange(0, len(objectTypeNames)), func(u int) string { + return objectTypeNames[u] + }) + + updateGen := rapid.Custom(func(t *rapid.T) schema.ObjectUpdate { + objectType := objectTypeSelector.Draw(t, "objectType") + objectColl, ok := objectCollections.Get(objectType) + require.True(t, ok) + return objectColl.UpdateGen().Draw(t, "update") + }) + + return &Module{ + moduleSchema: moduleSchema, + updateGen: updateGen, + objectCollections: objectCollections, + } +} + +func (o *Module) ApplyUpdate(update schema.ObjectUpdate) error { + objState, ok := o.objectCollections.Get(update.TypeName) + if !ok { + return fmt.Errorf("object type %s not found in module", update.TypeName) + } + + return objState.ApplyUpdate(update) +} + +func (o *Module) UpdateGen() *rapid.Generator[schema.ObjectUpdate] { + return o.updateGen +} + +func (o *Module) GetObjectCollection(objectType string) (*ObjectCollection, bool) { + return o.objectCollections.Get(objectType) +} + +func (o *Module) ScanState(f func(schema.ObjectUpdate) error) error { + var err error + o.objectCollections.Scan(func(key string, value *ObjectCollection) bool { + err = value.ScanState(func(update schema.ObjectUpdate) error { + return f(update) + }) + return err == nil + }) + return err +} diff --git a/schema/testing/statesim/object.go b/schema/testing/statesim/object.go new file mode 100644 index 000000000000..6b11361ea7ca --- /dev/null +++ b/schema/testing/statesim/object.go @@ -0,0 +1,117 @@ +package statesim + +import ( + "fmt" + + "github.com/tidwall/btree" + "pgregory.net/rapid" + + "cosmossdk.io/schema" + schematesting "cosmossdk.io/schema/testing" +) + +type ObjectCollection struct { + options Options + objectType schema.ObjectType + objects *btree.Map[string, schema.ObjectUpdate] + updateGen *rapid.Generator[schema.ObjectUpdate] + valueFieldIndices map[string]int +} + +func NewObjectCollection(objectType schema.ObjectType, options Options) *ObjectCollection { + objects := &btree.Map[string, schema.ObjectUpdate]{} + updateGen := schematesting.ObjectUpdateGen(objectType, objects) + valueFieldIndices := make(map[string]int, len(objectType.ValueFields)) + for i, field := range objectType.ValueFields { + valueFieldIndices[field.Name] = i + } + + return &ObjectCollection{ + options: options, + objectType: objectType, + objects: objects, + updateGen: updateGen, + valueFieldIndices: valueFieldIndices, + } +} + +func (o *ObjectCollection) ApplyUpdate(update schema.ObjectUpdate) error { + if update.TypeName != o.objectType.Name { + return fmt.Errorf("update type name %q does not match object type name %q", update.TypeName, o.objectType.Name) + } + + err := o.objectType.ValidateObjectUpdate(update) + if err != nil { + return err + } + + keyStr := fmt.Sprintf("%v", update.Key) + cur, exists := o.objects.Get(keyStr) + if update.Delete { + if o.objectType.RetainDeletions && o.options.CanRetainDeletions { + if !exists { + return fmt.Errorf("object not found for deletion: %v", update.Key) + } + + cur.Delete = true + o.objects.Set(keyStr, cur) + } else { + o.objects.Delete(keyStr) + } + } else { + // merge value updates only if we have more than one value field + if valueUpdates, ok := update.Value.(schema.ValueUpdates); ok && + len(o.objectType.ValueFields) > 1 { + var values []interface{} + if exists { + values = cur.Value.([]interface{}) + } else { + values = make([]interface{}, len(o.objectType.ValueFields)) + } + + err = valueUpdates.Iterate(func(fieldName string, value interface{}) bool { + fieldIndex, ok := o.valueFieldIndices[fieldName] + if !ok { + panic(fmt.Sprintf("field %q not found in object type %q", fieldName, o.objectType.Name)) + } + + values[fieldIndex] = value + return true + }) + if err != nil { + return err + } + + update.Value = values + } + + o.objects.Set(keyStr, update) + } + + return nil +} + +func (o *ObjectCollection) UpdateGen() *rapid.Generator[schema.ObjectUpdate] { + return o.updateGen +} + +func (o *ObjectCollection) ScanState(f func(schema.ObjectUpdate) error) error { + var err error + o.objects.Scan(func(_ string, v schema.ObjectUpdate) bool { + err = f(v) + return err == nil + }) + return err +} + +func (o *ObjectCollection) GetObject(key any) (schema.ObjectUpdate, bool) { + return o.objects.Get(fmt.Sprintf("%v", key)) +} + +func (o *ObjectCollection) ObjectType() schema.ObjectType { + return o.objectType +} + +func (o *ObjectCollection) Len() int { + return o.objects.Len() +} diff --git a/schema/testing/statesim/options.go b/schema/testing/statesim/options.go new file mode 100644 index 000000000000..946138ea7214 --- /dev/null +++ b/schema/testing/statesim/options.go @@ -0,0 +1,5 @@ +package statesim + +type Options struct { + CanRetainDeletions bool +} diff --git a/schema/unique.go b/schema/unique.go new file mode 100644 index 000000000000..9d6db7d88700 --- /dev/null +++ b/schema/unique.go @@ -0,0 +1,5 @@ +package schema + +type UniqueConstraint struct { + FieldNames []string +} From 6d0c8e1ea304aa2cd65e023112b782308b440874 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 19:22:07 +0200 Subject: [PATCH 34/79] deletions --- indexer/postgres/base.sql | 33 - indexer/postgres/graphql-proxy/.gitignore | 24 - indexer/postgres/graphql-proxy/README.md | 47 - .../postgres/graphql-proxy/astro.config.mjs | 6 - indexer/postgres/graphql-proxy/package.json | 19 - indexer/postgres/graphql-proxy/pnpm-lock.yaml | 4311 ----------------- .../postgres/graphql-proxy/public/favicon.svg | 9 - indexer/postgres/graphql-proxy/src/env.d.ts | 1 - .../graphql-proxy/src/pages/graphiql.astro | 81 - .../graphql-proxy/src/pages/graphql.ts | 47 - .../graphql-proxy/src/pages/index.astro | 11 - indexer/postgres/graphql-proxy/tsconfig.json | 3 - 12 files changed, 4592 deletions(-) delete mode 100644 indexer/postgres/base.sql delete mode 100644 indexer/postgres/graphql-proxy/.gitignore delete mode 100644 indexer/postgres/graphql-proxy/README.md delete mode 100644 indexer/postgres/graphql-proxy/astro.config.mjs delete mode 100644 indexer/postgres/graphql-proxy/package.json delete mode 100644 indexer/postgres/graphql-proxy/pnpm-lock.yaml delete mode 100644 indexer/postgres/graphql-proxy/public/favicon.svg delete mode 100644 indexer/postgres/graphql-proxy/src/env.d.ts delete mode 100644 indexer/postgres/graphql-proxy/src/pages/graphiql.astro delete mode 100644 indexer/postgres/graphql-proxy/src/pages/graphql.ts delete mode 100644 indexer/postgres/graphql-proxy/src/pages/index.astro delete mode 100644 indexer/postgres/graphql-proxy/tsconfig.json diff --git a/indexer/postgres/base.sql b/indexer/postgres/base.sql deleted file mode 100644 index b73d75140183..000000000000 --- a/indexer/postgres/base.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS indexer; - -CREATE TABLE IF NOT EXISTS indexer.indexed_modules -( - module_name TEXT NOT NULL PRIMARY KEY -); - -CREATE TABLE IF NOT EXISTS block -( - block_number BIGINT NOT NULL PRIMARY KEY, - header JSONB NULL -); - -CREATE TABLE IF NOT EXISTS tx -( - id BIGSERIAL PRIMARY KEY, - block_number BIGINT NOT NULL REFERENCES block (block_number), - tx_index BIGINT NOT NULL, - data JSONB NOT NULL -); - -CREATE TABLE IF NOT EXISTS event -( - id BIGSERIAL PRIMARY KEY, - block_number BIGINT NOT NULL REFERENCES block (block_number), - tx_index BIGINT NOT NULL REFERENCES tx (tx_index), - msg_idx BIGINT NOT NULL, - event_idx BIGINT NOT NULL, - type TEXT NOT NULL, - data JSONB NOT NULL -); - - diff --git a/indexer/postgres/graphql-proxy/.gitignore b/indexer/postgres/graphql-proxy/.gitignore deleted file mode 100644 index 16d54bb13c8a..000000000000 --- a/indexer/postgres/graphql-proxy/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# build output -dist/ -# generated types -.astro/ - -# dependencies -node_modules/ - -# logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - - -# environment variables -.env -.env.production - -# macOS-specific files -.DS_Store - -# jetbrains setting folder -.idea/ diff --git a/indexer/postgres/graphql-proxy/README.md b/indexer/postgres/graphql-proxy/README.md deleted file mode 100644 index e34a99b446b4..000000000000 --- a/indexer/postgres/graphql-proxy/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Astro Starter Kit: Minimal - -```sh -npm create astro@latest -- --template minimal -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json) - -> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! - -## 🚀 Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -├── public/ -├── src/ -│ └── pages/ -│ └── index.astro -└── package.json -``` - -Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. - -There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - -Any static assets, like images, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## 👀 Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/indexer/postgres/graphql-proxy/astro.config.mjs b/indexer/postgres/graphql-proxy/astro.config.mjs deleted file mode 100644 index 37b2bbe8c702..000000000000 --- a/indexer/postgres/graphql-proxy/astro.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'astro/config'; - -// https://astro.build/config -export default defineConfig({ - output: 'server' -}); diff --git a/indexer/postgres/graphql-proxy/package.json b/indexer/postgres/graphql-proxy/package.json deleted file mode 100644 index df3349512273..000000000000 --- a/indexer/postgres/graphql-proxy/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "graphql-proxy", - "type": "module", - "version": "0.0.1", - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro check && astro build", - "preview": "astro preview", - "astro": "astro" - }, - "dependencies": { - "@astrojs/check": "^0.7.0", - "@types/pg": "^8.11.6", - "astro": "^4.11.0", - "pg": "^8.12.0", - "typescript": "^5.5.2" - } -} \ No newline at end of file diff --git a/indexer/postgres/graphql-proxy/pnpm-lock.yaml b/indexer/postgres/graphql-proxy/pnpm-lock.yaml deleted file mode 100644 index f8ed5b7be957..000000000000 --- a/indexer/postgres/graphql-proxy/pnpm-lock.yaml +++ /dev/null @@ -1,4311 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@astrojs/check': - specifier: ^0.7.0 - version: 0.7.0(typescript@5.5.2) - '@types/pg': - specifier: ^8.11.6 - version: 8.11.6 - astro: - specifier: ^4.11.0 - version: 4.11.0(@types/node@20.14.7)(typescript@5.5.2) - pg: - specifier: ^8.12.0 - version: 8.12.0 - typescript: - specifier: ^5.5.2 - version: 5.5.2 - -packages: - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@astrojs/check@0.7.0': - resolution: {integrity: sha512-UTqwOeKNu9IYZmJXEeWnQuTdSd/pX58Hl4TUARsMlT97SVDL//kLBE4T/ctxRz6J573N87oE5ddtW/uOOnQTug==} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - - '@astrojs/compiler@2.8.0': - resolution: {integrity: sha512-yrpD1WRGqsJwANaDIdtHo+YVjvIOFAjC83lu5qENIgrafwZcJgSXDuwVMXOgok4tFzpeKLsFQ6c3FoUdloLWBQ==} - - '@astrojs/internal-helpers@0.4.0': - resolution: {integrity: sha512-6B13lz5n6BrbTqCTwhXjJXuR1sqiX/H6rTxzlXx+lN1NnV4jgnq/KJldCQaUWJzPL5SiWahQyinxAbxQtwgPHA==} - - '@astrojs/language-server@2.10.0': - resolution: {integrity: sha512-crHXpqYfA5qWioiuZnZFpTsNItgBlF1f0S9MzDYS7/pfCALkHNJ7K3w9U/j0uMKymsT4hC7BfMaX0DYlfdSzHg==} - hasBin: true - peerDependencies: - prettier: ^3.0.0 - prettier-plugin-astro: '>=0.11.0' - peerDependenciesMeta: - prettier: - optional: true - prettier-plugin-astro: - optional: true - - '@astrojs/markdown-remark@5.1.0': - resolution: {integrity: sha512-S6Z3K2hOB7MfjeDoHsotnP/q2UsnEDB8NlNAaCjMDsGBZfTUbWxyLW3CaphEWw08f6KLZi2ibK9yC3BaMhh2NQ==} - - '@astrojs/prism@3.1.0': - resolution: {integrity: sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==} - engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} - - '@astrojs/telemetry@3.1.0': - resolution: {integrity: sha512-/ca/+D8MIKEC8/A9cSaPUqQNZm+Es/ZinRv0ZAzvu2ios7POQSsVD+VOj7/hypWNsNM3T7RpfgNq7H2TU1KEHA==} - engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} - - '@babel/code-frame@7.24.7': - resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.24.7': - resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.24.7': - resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.24.7': - resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-annotate-as-pure@7.24.7': - resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.24.7': - resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-environment-visitor@7.24.7': - resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.24.7': - resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.24.7': - resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.24.7': - resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.24.7': - resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.24.7': - resolution: {integrity: sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-simple-access@7.24.7': - resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-split-export-declaration@7.24.7': - resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.24.7': - resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.24.7': - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.24.7': - resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.24.7': - resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.24.7': - resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.24.7': - resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-syntax-jsx@7.24.7': - resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx@7.24.7': - resolution: {integrity: sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.24.7': - resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.24.7': - resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.24.7': - resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} - engines: {node: '>=6.9.0'} - - '@emmetio/abbreviation@2.3.3': - resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} - - '@emmetio/css-abbreviation@2.1.8': - resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} - - '@emmetio/css-parser@0.4.0': - resolution: {integrity: sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw==} - - '@emmetio/html-matcher@1.3.0': - resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} - - '@emmetio/scanner@1.0.4': - resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} - - '@emmetio/stream-reader-utils@0.1.0': - resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} - - '@emmetio/stream-reader@2.2.0': - resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} - - '@emnapi/runtime@1.2.0': - resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==} - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@img/sharp-darwin-arm64@0.33.4': - resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.33.4': - resolution: {integrity: sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.0.2': - resolution: {integrity: sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==} - engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.0.2': - resolution: {integrity: sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==} - engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.0.2': - resolution: {integrity: sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==} - engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linux-arm@1.0.2': - resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==} - engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.0.2': - resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==} - engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [s390x] - os: [linux] - - '@img/sharp-libvips-linux-x64@1.0.2': - resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==} - engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-arm64@1.0.2': - resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==} - engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-x64@1.0.2': - resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==} - engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-linux-arm64@0.33.4': - resolution: {integrity: sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-linux-arm@0.33.4': - resolution: {integrity: sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==} - engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm] - os: [linux] - - '@img/sharp-linux-s390x@0.33.4': - resolution: {integrity: sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==} - engines: {glibc: '>=2.31', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [s390x] - os: [linux] - - '@img/sharp-linux-x64@0.33.4': - resolution: {integrity: sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-linuxmusl-arm64@0.33.4': - resolution: {integrity: sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==} - engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-linuxmusl-x64@0.33.4': - resolution: {integrity: sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==} - engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-wasm32@0.33.4': - resolution: {integrity: sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [wasm32] - - '@img/sharp-win32-ia32@0.33.4': - resolution: {integrity: sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.33.4': - resolution: {integrity: sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [win32] - - '@johnsoncodehk/vscode-html-languageservice@5.2.0-34a5462': - resolution: {integrity: sha512-etqLfpSJ5zaw76KUNF603be6d6QsiQPmaHr9FKEp4zhLZJzWCCMH6Icak7MtLUFLZLMpL761mZNImi/joBo1ZA==} - - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.4.15': - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@rollup/rollup-android-arm-eabi@4.18.0': - resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.18.0': - resolution: {integrity: sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.18.0': - resolution: {integrity: sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.18.0': - resolution: {integrity: sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-linux-arm-gnueabihf@4.18.0': - resolution: {integrity: sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.18.0': - resolution: {integrity: sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.18.0': - resolution: {integrity: sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.18.0': - resolution: {integrity: sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': - resolution: {integrity: sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.18.0': - resolution: {integrity: sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.18.0': - resolution: {integrity: sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.18.0': - resolution: {integrity: sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.18.0': - resolution: {integrity: sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.18.0': - resolution: {integrity: sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.18.0': - resolution: {integrity: sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.18.0': - resolution: {integrity: sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==} - cpu: [x64] - os: [win32] - - '@shikijs/core@1.9.0': - resolution: {integrity: sha512-cbSoY8P/jgGByG8UOl3jnP/CWg/Qk+1q+eAKWtcrU3pNoILF8wTsLB0jT44qUBV8Ce1SvA9uqcM9Xf+u3fJFBw==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.20.6': - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/ms@0.7.34': - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - - '@types/nlcst@1.0.4': - resolution: {integrity: sha512-ABoYdNQ/kBSsLvZAekMhIPMQ3YUZvavStpKYs7BjLLuKVmIMA0LUgZ7b54zzuWJRbHF80v1cNf4r90Vd6eMQDg==} - - '@types/node@20.14.7': - resolution: {integrity: sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==} - - '@types/pg@8.11.6': - resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} - - '@types/unist@2.0.10': - resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} - - '@types/unist@3.0.2': - resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} - - '@ungap/structured-clone@1.2.0': - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - - '@volar/kit@2.2.5': - resolution: {integrity: sha512-Bmn0UCaT43xUGGRwcmFG9lKhiCCLjRT4ScSLLPn5C9ltUcSGnIFFDlbZZa1PreHYHq25/4zkXt9Ap32klAh17w==} - peerDependencies: - typescript: '*' - - '@volar/language-core@2.2.5': - resolution: {integrity: sha512-2htyAuxRrAgETmFeUhT4XLELk3LiEcqoW/B8YUXMF6BrGWLMwIR09MFaZYvrA2UhbdAeSyeQ726HaWSWkexUcQ==} - - '@volar/language-server@2.2.5': - resolution: {integrity: sha512-PV/jkUkI+m72HTXwnY7hsGqLY3VNi96ZRoWFRzVC9QG/853bixxjveXPJIiydMJ9I739lO3kcj3hnGrF5Sm+HA==} - - '@volar/language-service@2.2.5': - resolution: {integrity: sha512-a97e/0uCe+uSu23F4zvgvldqJtZe6jugQeEHWjTfhgOEO8+Be0t5CZNNVItQqmPyAsD8eElg0S/cP6uxvCmCSQ==} - - '@volar/snapshot-document@2.2.5': - resolution: {integrity: sha512-MTOvWVKxM7ugKO3Amffkv2pND03fe2JtfygYaputqjVFML7YxtTXj8SPnI2pODLeSwOKzDYL6Q8r5j6Y5AgUzQ==} - - '@volar/source-map@2.2.5': - resolution: {integrity: sha512-wrOEIiZNf4E+PWB0AxyM4tfhkfldPsb3bxg8N6FHrxJH2ohar7aGu48e98bp3pR9HUA7P/pR9VrLmkTrgCCnWQ==} - - '@volar/typescript@2.2.5': - resolution: {integrity: sha512-eSV/n75+ppfEVugMC/salZsI44nXDPAyL6+iTYCNLtiLHGJsnMv9GwiDMujrvAUj/aLQyqRJgYtXRoxop2clCw==} - - '@vscode/emmet-helper@2.9.3': - resolution: {integrity: sha512-rB39LHWWPQYYlYfpv9qCoZOVioPCftKXXqrsyqN1mTWZM6dTnONT63Db+03vgrBbHzJN45IrgS/AGxw9iiqfEw==} - - '@vscode/l10n@0.0.16': - resolution: {integrity: sha512-JT5CvrIYYCrmB+dCana8sUqJEcGB1ZDXNLMQ2+42bW995WmNoenijWMUdZfwmuQUTQcEVVIa2OecZzTYWUW9Cg==} - - '@vscode/l10n@0.0.18': - resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} - - acorn@8.12.0: - resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - - array-iterate@2.0.1: - resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} - - astro@4.11.0: - resolution: {integrity: sha512-3VWxz/08sChQIX68tuE7Y769DUdjsT3Zq2/y4SkrDRlwN9IZ/aebwcRWr5a2yMSdO2vpFxtEdobq0mKnMlLErg==} - engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} - hasBin: true - - axobject-query@4.0.0: - resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} - - bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - - base-64@1.0.0: - resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - boxen@7.1.1: - resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} - engines: {node: '>=14.16'} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.23.1: - resolution: {integrity: sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - camelcase@7.0.1: - resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} - engines: {node: '>=14.16'} - - caniuse-lite@1.0.30001636: - resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} - - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - ci-info@4.0.0: - resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} - engines: {node: '>=8'} - - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - - cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - - common-ancestor-path@1.0.1: - resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - debug@4.3.5: - resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decode-named-character-reference@1.0.2: - resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - - deterministic-object-hash@2.0.2: - resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} - engines: {node: '>=18'} - - devalue@5.0.0: - resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} - - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - - diff@5.2.0: - resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} - engines: {node: '>=0.3.1'} - - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - - dset@3.1.3: - resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} - engines: {node: '>=4'} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - electron-to-chromium@1.4.808: - resolution: {integrity: sha512-0ItWyhPYnww2VOuCGF4s1LTfbrdAV2ajy/TN+ZTuhR23AHI6rWHCrBXJ/uxoXOvRRqw8qjYVrG81HFI7x/2wdQ==} - - emmet@2.4.7: - resolution: {integrity: sha512-O5O5QNqtdlnQM2bmKHtJgyChcrFMgQuulI+WdiOw2NArzprUqqxUW6bgYtKvzKgrsYpuLWalOkdhNP+1jluhCA==} - - emoji-regex@10.3.0: - resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - es-module-lexer@1.5.3: - resolution: {integrity: sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==} - - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - find-yarn-workspace-root2@1.2.16: - resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} - - flattie@1.1.1: - resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} - engines: {node: '>=8'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-east-asian-width@1.2.0: - resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} - engines: {node: '>=18'} - - get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - - github-slugger@2.0.0: - resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hast-util-from-html@2.0.1: - resolution: {integrity: sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==} - - hast-util-from-parse5@8.0.1: - resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} - - hast-util-is-element@3.0.0: - resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - - hast-util-parse-selector@4.0.0: - resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} - - hast-util-raw@9.0.4: - resolution: {integrity: sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==} - - hast-util-to-html@9.0.1: - resolution: {integrity: sha512-hZOofyZANbyWo+9RP75xIDV/gq+OUKx+T46IlwERnKmfpwp81XBFbT9mi26ws+SJchA4RVUQwIBJpqEOBhMzEQ==} - - hast-util-to-parse5@8.0.0: - resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} - - hast-util-to-text@4.0.2: - resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - - hastscript@8.0.0: - resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} - - html-escaper@3.0.3: - resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} - - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - - import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} - - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-buffer@2.0.5: - resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} - engines: {node: '>=4'} - - is-core-module@2.14.0: - resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} - engines: {node: '>= 0.4'} - - is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - - is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - - is-unicode-supported@2.0.0: - resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} - engines: {node: '>=18'} - - is-wsl@3.1.0: - resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} - engines: {node: '>=16'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonc-parser@2.3.1: - resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} - - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - - load-yaml-file@0.2.0: - resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} - engines: {node: '>=6'} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} - engines: {node: '>=18'} - - longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - magic-string@0.30.10: - resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} - - markdown-table@3.0.3: - resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} - - mdast-util-definitions@6.0.0: - resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} - - mdast-util-find-and-replace@3.0.1: - resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} - - mdast-util-from-markdown@2.0.1: - resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} - - mdast-util-gfm-autolink-literal@2.0.0: - resolution: {integrity: sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==} - - mdast-util-gfm-footnote@2.0.0: - resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} - - mdast-util-gfm-strikethrough@2.0.0: - resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} - - mdast-util-gfm-table@2.0.0: - resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} - - mdast-util-gfm-task-list-item@2.0.0: - resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} - - mdast-util-gfm@3.0.0: - resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} - - mdast-util-phrasing@4.1.0: - resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - - mdast-util-to-hast@13.2.0: - resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} - - mdast-util-to-markdown@2.1.0: - resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==} - - mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromark-core-commonmark@2.0.1: - resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==} - - micromark-extension-gfm-autolink-literal@2.0.0: - resolution: {integrity: sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==} - - micromark-extension-gfm-footnote@2.0.0: - resolution: {integrity: sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==} - - micromark-extension-gfm-strikethrough@2.0.0: - resolution: {integrity: sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==} - - micromark-extension-gfm-table@2.0.0: - resolution: {integrity: sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==} - - micromark-extension-gfm-tagfilter@2.0.0: - resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} - - micromark-extension-gfm-task-list-item@2.0.1: - resolution: {integrity: sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==} - - micromark-extension-gfm@3.0.0: - resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - - micromark-factory-destination@2.0.0: - resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} - - micromark-factory-label@2.0.0: - resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} - - micromark-factory-space@2.0.0: - resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} - - micromark-factory-title@2.0.0: - resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} - - micromark-factory-whitespace@2.0.0: - resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} - - micromark-util-character@2.1.0: - resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} - - micromark-util-chunked@2.0.0: - resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} - - micromark-util-classify-character@2.0.0: - resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} - - micromark-util-combine-extensions@2.0.0: - resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} - - micromark-util-decode-numeric-character-reference@2.0.1: - resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} - - micromark-util-decode-string@2.0.0: - resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} - - micromark-util-encode@2.0.0: - resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} - - micromark-util-html-tag-name@2.0.0: - resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} - - micromark-util-normalize-identifier@2.0.0: - resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} - - micromark-util-resolve-all@2.0.0: - resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} - - micromark-util-sanitize-uri@2.0.0: - resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} - - micromark-util-subtokenize@2.0.1: - resolution: {integrity: sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==} - - micromark-util-symbol@2.0.0: - resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} - - micromark-util-types@2.0.0: - resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} - - micromark@4.0.0: - resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} - - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} - engines: {node: '>=8.6'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - - mrmime@2.0.0: - resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} - engines: {node: '>=10'} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - muggle-string@0.4.1: - resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nlcst-to-string@3.1.1: - resolution: {integrity: sha512-63mVyqaqt0cmn2VcI2aH6kxe1rLAmSROqHMA0i4qqg1tidkfExgpb0FGMikMCn86mw5dFtBtEANfmSSK7TjNHw==} - - node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - obuf@1.1.2: - resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - - ora@8.0.1: - resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} - engines: {node: '>=18'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - p-queue@8.0.1: - resolution: {integrity: sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==} - engines: {node: '>=18'} - - p-timeout@6.1.2: - resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} - engines: {node: '>=14.16'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - parse-latin@5.0.1: - resolution: {integrity: sha512-b/K8ExXaWC9t34kKeDV8kGXBkXZ1HCSAZRYE7HR14eA1GlXX5L8iWhs8USJNhQU9q5ci413jCKF0gOyovvyRBg==} - - parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} - - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-to-regexp@6.2.2: - resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} - - pg-cloudflare@1.1.1: - resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} - - pg-connection-string@2.6.4: - resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==} - - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-numeric@1.0.2: - resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} - engines: {node: '>=4'} - - pg-pool@3.6.2: - resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==} - peerDependencies: - pg: '>=8.0' - - pg-protocol@1.6.1: - resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - - pg-types@4.0.2: - resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} - engines: {node: '>=10'} - - pg@8.12.0: - resolution: {integrity: sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==} - engines: {node: '>= 8.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - - pgpass@1.0.5: - resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - - picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - - postcss@8.4.38: - resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} - engines: {node: ^10 || ^12 || >=14} - - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-array@3.0.2: - resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} - engines: {node: '>=12'} - - postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} - engines: {node: '>=0.10.0'} - - postgres-bytea@3.0.0: - resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} - engines: {node: '>= 6'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-date@2.1.0: - resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} - engines: {node: '>=12'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - - postgres-interval@3.0.0: - resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} - engines: {node: '>=12'} - - postgres-range@1.1.4: - resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - - preferred-pm@3.1.3: - resolution: {integrity: sha512-MkXsENfftWSRpzCzImcp4FRsCc3y1opwB73CfCNWyzMqArju2CrlMHlqB7VexKiPEOjGMbttv1r9fSCn5S610w==} - engines: {node: '>=10'} - - prismjs@1.29.0: - resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} - engines: {node: '>=6'} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - property-information@6.5.0: - resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - rehype-parse@9.0.0: - resolution: {integrity: sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==} - - rehype-raw@7.0.0: - resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} - - rehype-stringify@10.0.0: - resolution: {integrity: sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==} - - rehype@13.0.1: - resolution: {integrity: sha512-AcSLS2mItY+0fYu9xKxOu1LhUZeBZZBx8//5HKzF+0XP+eP8+6a5MXn2+DW2kfXR6Dtp1FEXMVrjyKAcvcU8vg==} - - remark-gfm@4.0.0: - resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} - - remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - - remark-rehype@11.1.0: - resolution: {integrity: sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==} - - remark-smartypants@2.1.0: - resolution: {integrity: sha512-qoF6Vz3BjU2tP6OfZqHOvCU0ACmu/6jhGaINSQRI9mM7wCxNQTKB3JUAN4SVoN2ybElEDTxBIABRep7e569iJw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - remark-stringify@11.0.0: - resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - - request-light@0.7.0: - resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - - restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - retext-latin@3.1.0: - resolution: {integrity: sha512-5MrD1tuebzO8ppsja5eEu+ZbBeUNCjoEarn70tkXOS7Bdsdf6tNahsv2bY0Z8VooFF6cw7/6S+d3yI/TMlMVVQ==} - - retext-smartypants@5.2.0: - resolution: {integrity: sha512-Do8oM+SsjrbzT2UNIKgheP0hgUQTDDQYyZaIY3kfq0pdFzoPk+ZClYJ+OERNXveog4xf1pZL4PfRxNoVL7a/jw==} - - retext-stringify@3.1.0: - resolution: {integrity: sha512-767TLOaoXFXyOnjx/EggXlb37ZD2u4P1n0GJqVdpipqACsQP+20W+BNpMYrlJkq7hxffnFk+jc6mAK9qrbuB8w==} - - retext@8.1.0: - resolution: {integrity: sha512-N9/Kq7YTn6ZpzfiGW45WfEGJqFf1IM1q8OsRa1CGzIebCJBNCANDRmOrholiDRGKo/We7ofKR4SEvcGAWEMD3Q==} - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@4.18.0: - resolution: {integrity: sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.6.2: - resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} - engines: {node: '>=10'} - hasBin: true - - sharp@0.33.4: - resolution: {integrity: sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==} - engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shiki@1.9.0: - resolution: {integrity: sha512-i6//Lqgn7+7nZA0qVjoYH0085YdNk4MC+tJV4bo+HgjgRMJ0JmkLZzFAuvVioJqLkcGDK5GAMpghZEZkCnwxpQ==} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - - split2@4.2.0: - resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} - engines: {node: '>= 10.x'} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} - engines: {node: '>=18'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string-width@7.1.0: - resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==} - engines: {node: '>=18'} - - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - - trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - - tsconfck@3.1.0: - resolution: {integrity: sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - tslib@2.6.3: - resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - - typesafe-path@0.2.2: - resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} - - typescript-auto-import-cache@0.3.3: - resolution: {integrity: sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA==} - - typescript@5.5.2: - resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - unherit@3.0.1: - resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==} - - unified@10.1.2: - resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} - - unified@11.0.5: - resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - - unist-util-find-after@5.0.0: - resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} - - unist-util-is@5.2.1: - resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} - - unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} - - unist-util-modify-children@3.1.1: - resolution: {integrity: sha512-yXi4Lm+TG5VG+qvokP6tpnk+r1EPwyYL04JWDxLvgvPV40jANh7nm3udk65OOWquvbMDe+PL9+LmkxDpTv/7BA==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-remove-position@5.0.0: - resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} - - unist-util-stringify-position@3.0.3: - resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-children@2.0.2: - resolution: {integrity: sha512-+LWpMFqyUwLGpsQxpumsQ9o9DG2VGLFrpz+rpVXYIEdPy57GSy5HioC0g3bg/8WP9oCLlapQtklOzQ8uLS496Q==} - - unist-util-visit-parents@5.1.3: - resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} - - unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} - - unist-util-visit@4.1.2: - resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} - - unist-util-visit@5.0.0: - resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - - update-browserslist-db@1.0.16: - resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - vfile-location@5.0.2: - resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} - - vfile-message@3.1.4: - resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} - - vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} - - vfile@5.3.7: - resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} - - vfile@6.0.1: - resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} - - vite@5.3.1: - resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vitefu@0.2.5: - resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - vite: - optional: true - - volar-service-css@0.0.45: - resolution: {integrity: sha512-f+AlUI1+kESbcZSVaNJVAnK0c/9Da5StoxzPqA5/8VqUHJWNdubWNnwG5xpFVTfgh6pgTcey3UBhBfHytFaIOg==} - peerDependencies: - '@volar/language-service': ~2.2.3 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-emmet@0.0.45: - resolution: {integrity: sha512-9nLXSDkR1vA/3fQkFEsSXAu3XovQxOpTkVG2jilQgfek/K1ZLkaA/WMhN/TtmPmQg4NxE9Ni6mA5udBQ5gVXIA==} - peerDependencies: - '@volar/language-service': ~2.2.3 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-html@0.0.45: - resolution: {integrity: sha512-tLTJqfy1v5C4nmeAsfekFIKPl4r4qDMyL0L9MWywr/EApZzPCsbeUGxCqdzxSMC2q7PMCfX2i167txDo+J0LVA==} - peerDependencies: - '@volar/language-service': ~2.2.3 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-prettier@0.0.45: - resolution: {integrity: sha512-+mBS2EsDgp/kunKEBnHvhBwIQm5v2ahw4NKpKdg4sTpXy3UxqHt+Fq/wRYQ7Z8LlNVNRVfp75ThjM+w2zaZBAw==} - peerDependencies: - '@volar/language-service': ~2.2.3 - prettier: ^2.2 || ^3.0 - peerDependenciesMeta: - '@volar/language-service': - optional: true - prettier: - optional: true - - volar-service-typescript-twoslash-queries@0.0.45: - resolution: {integrity: sha512-KrPUUvKggZgV9mrDpstCzmf20irgv0ooMv+FGDzIIQUkya+d2+nSS8Mx2h9FvsYgLccUVw5jU3Rhwhd3pv/7qg==} - peerDependencies: - '@volar/language-service': ~2.2.3 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - volar-service-typescript@0.0.45: - resolution: {integrity: sha512-i/mMIIAMastJ2kgPo3qvX0Rrl7NyxhIYZ0ug/B4ambZcLPI1vzBgS2fmvyWX3jhBYHh8NmbAotFj+0Y9JtN47A==} - peerDependencies: - '@volar/language-service': ~2.2.3 - peerDependenciesMeta: - '@volar/language-service': - optional: true - - vscode-css-languageservice@6.2.14: - resolution: {integrity: sha512-5UPQ9Y1sUTnuMyaMBpO7LrBkqjhEJb5eAwdUlDp+Uez8lry+Tspnk3+3p2qWS4LlNsr4p3v9WkZxUf1ltgFpgw==} - - vscode-html-languageservice@5.2.0: - resolution: {integrity: sha512-cdNMhyw57/SQzgUUGSIMQ66jikqEN6nBNyhx5YuOyj9310+eY9zw8Q0cXpiKzDX8aHYFewQEXRnigl06j/TVwQ==} - - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-textdocument@1.0.11: - resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - - vscode-nls@5.2.0: - resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} - - vscode-uri@2.1.2: - resolution: {integrity: sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==} - - vscode-uri@3.0.8: - resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - - web-namespaces@2.0.1: - resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - - which-pm-runs@1.1.0: - resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} - engines: {node: '>=4'} - - which-pm@2.0.0: - resolution: {integrity: sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==} - engines: {node: '>=8.15'} - - which-pm@2.2.0: - resolution: {integrity: sha512-MOiaDbA5ZZgUjkeMWM5EkJp4loW5ZRoa5bc3/aeMox/PJelMhE6t7S/mLuiY43DBupyxH+S0U1bTui9kWUlmsw==} - engines: {node: '>=8.15'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - widest-line@4.0.1: - resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} - engines: {node: '>=12'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - - zod-to-json-schema@3.23.1: - resolution: {integrity: sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==} - peerDependencies: - zod: ^3.23.3 - - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - -snapshots: - - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - - '@astrojs/check@0.7.0(typescript@5.5.2)': - dependencies: - '@astrojs/language-server': 2.10.0(typescript@5.5.2) - chokidar: 3.6.0 - fast-glob: 3.3.2 - kleur: 4.1.5 - typescript: 5.5.2 - yargs: 17.7.2 - transitivePeerDependencies: - - prettier - - prettier-plugin-astro - - '@astrojs/compiler@2.8.0': {} - - '@astrojs/internal-helpers@0.4.0': {} - - '@astrojs/language-server@2.10.0(typescript@5.5.2)': - dependencies: - '@astrojs/compiler': 2.8.0 - '@jridgewell/sourcemap-codec': 1.4.15 - '@volar/kit': 2.2.5(typescript@5.5.2) - '@volar/language-core': 2.2.5 - '@volar/language-server': 2.2.5 - '@volar/language-service': 2.2.5 - '@volar/typescript': 2.2.5 - fast-glob: 3.3.2 - volar-service-css: 0.0.45(@volar/language-service@2.2.5) - volar-service-emmet: 0.0.45(@volar/language-service@2.2.5) - volar-service-html: 0.0.45(@volar/language-service@2.2.5) - volar-service-prettier: 0.0.45(@volar/language-service@2.2.5) - volar-service-typescript: 0.0.45(@volar/language-service@2.2.5) - volar-service-typescript-twoslash-queries: 0.0.45(@volar/language-service@2.2.5) - vscode-html-languageservice: 5.2.0 - vscode-uri: 3.0.8 - transitivePeerDependencies: - - typescript - - '@astrojs/markdown-remark@5.1.0': - dependencies: - '@astrojs/prism': 3.1.0 - github-slugger: 2.0.0 - hast-util-from-html: 2.0.1 - hast-util-to-text: 4.0.2 - import-meta-resolve: 4.1.0 - mdast-util-definitions: 6.0.0 - rehype-raw: 7.0.0 - rehype-stringify: 10.0.0 - remark-gfm: 4.0.0 - remark-parse: 11.0.0 - remark-rehype: 11.1.0 - remark-smartypants: 2.1.0 - shiki: 1.9.0 - unified: 11.0.5 - unist-util-remove-position: 5.0.0 - unist-util-visit: 5.0.0 - unist-util-visit-parents: 6.0.1 - vfile: 6.0.1 - transitivePeerDependencies: - - supports-color - - '@astrojs/prism@3.1.0': - dependencies: - prismjs: 1.29.0 - - '@astrojs/telemetry@3.1.0': - dependencies: - ci-info: 4.0.0 - debug: 4.3.5 - dlv: 1.1.3 - dset: 3.1.3 - is-docker: 3.0.0 - is-wsl: 3.1.0 - which-pm-runs: 1.1.0 - transitivePeerDependencies: - - supports-color - - '@babel/code-frame@7.24.7': - dependencies: - '@babel/highlight': 7.24.7 - picocolors: 1.0.1 - - '@babel/compat-data@7.24.7': {} - - '@babel/core@7.24.7': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/helper-compilation-targets': 7.24.7 - '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) - '@babel/helpers': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - convert-source-map: 2.0.0 - debug: 4.3.5 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.24.7': - dependencies: - '@babel/types': 7.24.7 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - - '@babel/helper-annotate-as-pure@7.24.7': - dependencies: - '@babel/types': 7.24.7 - - '@babel/helper-compilation-targets@7.24.7': - dependencies: - '@babel/compat-data': 7.24.7 - '@babel/helper-validator-option': 7.24.7 - browserslist: 4.23.1 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-environment-visitor@7.24.7': - dependencies: - '@babel/types': 7.24.7 - - '@babel/helper-function-name@7.24.7': - dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.7 - - '@babel/helper-hoist-variables@7.24.7': - dependencies: - '@babel/types': 7.24.7 - - '@babel/helper-module-imports@7.24.7': - dependencies: - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-simple-access': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.24.7': {} - - '@babel/helper-simple-access@7.24.7': - dependencies: - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-split-export-declaration@7.24.7': - dependencies: - '@babel/types': 7.24.7 - - '@babel/helper-string-parser@7.24.7': {} - - '@babel/helper-validator-identifier@7.24.7': {} - - '@babel/helper-validator-option@7.24.7': {} - - '@babel/helpers@7.24.7': - dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.7 - - '@babel/highlight@7.24.7': - dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.0.1 - - '@babel/parser@7.24.7': - dependencies: - '@babel/types': 7.24.7 - - '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-transform-react-jsx@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.7) - '@babel/types': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/template@7.24.7': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 - - '@babel/traverse@7.24.7': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 - debug: 4.3.5 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.24.7': - dependencies: - '@babel/helper-string-parser': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 - to-fast-properties: 2.0.0 - - '@emmetio/abbreviation@2.3.3': - dependencies: - '@emmetio/scanner': 1.0.4 - - '@emmetio/css-abbreviation@2.1.8': - dependencies: - '@emmetio/scanner': 1.0.4 - - '@emmetio/css-parser@0.4.0': - dependencies: - '@emmetio/stream-reader': 2.2.0 - '@emmetio/stream-reader-utils': 0.1.0 - - '@emmetio/html-matcher@1.3.0': - dependencies: - '@emmetio/scanner': 1.0.4 - - '@emmetio/scanner@1.0.4': {} - - '@emmetio/stream-reader-utils@0.1.0': {} - - '@emmetio/stream-reader@2.2.0': {} - - '@emnapi/runtime@1.2.0': - dependencies: - tslib: 2.6.3 - optional: true - - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - - '@img/sharp-darwin-arm64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.2 - optional: true - - '@img/sharp-darwin-x64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.2 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.0.2': - optional: true - - '@img/sharp-libvips-darwin-x64@1.0.2': - optional: true - - '@img/sharp-libvips-linux-arm64@1.0.2': - optional: true - - '@img/sharp-libvips-linux-arm@1.0.2': - optional: true - - '@img/sharp-libvips-linux-s390x@1.0.2': - optional: true - - '@img/sharp-libvips-linux-x64@1.0.2': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.0.2': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.0.2': - optional: true - - '@img/sharp-linux-arm64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.2 - optional: true - - '@img/sharp-linux-arm@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.2 - optional: true - - '@img/sharp-linux-s390x@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.2 - optional: true - - '@img/sharp-linux-x64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.2 - optional: true - - '@img/sharp-linuxmusl-arm64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 - optional: true - - '@img/sharp-linuxmusl-x64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.2 - optional: true - - '@img/sharp-wasm32@0.33.4': - dependencies: - '@emnapi/runtime': 1.2.0 - optional: true - - '@img/sharp-win32-ia32@0.33.4': - optional: true - - '@img/sharp-win32-x64@0.33.4': - optional: true - - '@johnsoncodehk/vscode-html-languageservice@5.2.0-34a5462': - dependencies: - '@vscode/l10n': 0.0.18 - vscode-languageserver-textdocument: 1.0.11 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.0.8 - - '@jridgewell/gen-mapping@0.3.5': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/sourcemap-codec@1.4.15': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - - '@rollup/rollup-android-arm-eabi@4.18.0': - optional: true - - '@rollup/rollup-android-arm64@4.18.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.18.0': - optional: true - - '@rollup/rollup-darwin-x64@4.18.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.18.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.18.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.18.0': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.18.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.18.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.18.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.18.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.18.0': - optional: true - - '@shikijs/core@1.9.0': {} - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 - '@types/babel__generator': 7.6.8 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 - - '@types/babel__generator@7.6.8': - dependencies: - '@babel/types': 7.24.7 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.24.7 - '@babel/types': 7.24.7 - - '@types/babel__traverse@7.20.6': - dependencies: - '@babel/types': 7.24.7 - - '@types/cookie@0.6.0': {} - - '@types/debug@4.1.12': - dependencies: - '@types/ms': 0.7.34 - - '@types/estree@1.0.5': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.2 - - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.2 - - '@types/ms@0.7.34': {} - - '@types/nlcst@1.0.4': - dependencies: - '@types/unist': 2.0.10 - - '@types/node@20.14.7': - dependencies: - undici-types: 5.26.5 - - '@types/pg@8.11.6': - dependencies: - '@types/node': 20.14.7 - pg-protocol: 1.6.1 - pg-types: 4.0.2 - - '@types/unist@2.0.10': {} - - '@types/unist@3.0.2': {} - - '@ungap/structured-clone@1.2.0': {} - - '@volar/kit@2.2.5(typescript@5.5.2)': - dependencies: - '@volar/language-service': 2.2.5 - '@volar/typescript': 2.2.5 - typesafe-path: 0.2.2 - typescript: 5.5.2 - vscode-languageserver-textdocument: 1.0.11 - vscode-uri: 3.0.8 - - '@volar/language-core@2.2.5': - dependencies: - '@volar/source-map': 2.2.5 - - '@volar/language-server@2.2.5': - dependencies: - '@volar/language-core': 2.2.5 - '@volar/language-service': 2.2.5 - '@volar/snapshot-document': 2.2.5 - '@volar/typescript': 2.2.5 - '@vscode/l10n': 0.0.16 - path-browserify: 1.0.1 - request-light: 0.7.0 - vscode-languageserver: 9.0.1 - vscode-languageserver-protocol: 3.17.5 - vscode-languageserver-textdocument: 1.0.11 - vscode-uri: 3.0.8 - - '@volar/language-service@2.2.5': - dependencies: - '@volar/language-core': 2.2.5 - vscode-languageserver-protocol: 3.17.5 - vscode-languageserver-textdocument: 1.0.11 - vscode-uri: 3.0.8 - - '@volar/snapshot-document@2.2.5': - dependencies: - vscode-languageserver-protocol: 3.17.5 - vscode-languageserver-textdocument: 1.0.11 - - '@volar/source-map@2.2.5': - dependencies: - muggle-string: 0.4.1 - - '@volar/typescript@2.2.5': - dependencies: - '@volar/language-core': 2.2.5 - path-browserify: 1.0.1 - - '@vscode/emmet-helper@2.9.3': - dependencies: - emmet: 2.4.7 - jsonc-parser: 2.3.1 - vscode-languageserver-textdocument: 1.0.11 - vscode-languageserver-types: 3.17.5 - vscode-uri: 2.1.2 - - '@vscode/l10n@0.0.16': {} - - '@vscode/l10n@0.0.18': {} - - acorn@8.12.0: {} - - ansi-align@3.0.1: - dependencies: - string-width: 4.2.3 - - ansi-regex@5.0.1: {} - - ansi-regex@6.0.1: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.1: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - - array-iterate@2.0.1: {} - - astro@4.11.0(@types/node@20.14.7)(typescript@5.5.2): - dependencies: - '@astrojs/compiler': 2.8.0 - '@astrojs/internal-helpers': 0.4.0 - '@astrojs/markdown-remark': 5.1.0 - '@astrojs/telemetry': 3.1.0 - '@babel/core': 7.24.7 - '@babel/generator': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.7) - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - '@types/babel__core': 7.20.5 - '@types/cookie': 0.6.0 - acorn: 8.12.0 - aria-query: 5.3.0 - axobject-query: 4.0.0 - boxen: 7.1.1 - chokidar: 3.6.0 - ci-info: 4.0.0 - clsx: 2.1.1 - common-ancestor-path: 1.0.1 - cookie: 0.6.0 - cssesc: 3.0.0 - debug: 4.3.5 - deterministic-object-hash: 2.0.2 - devalue: 5.0.0 - diff: 5.2.0 - dlv: 1.1.3 - dset: 3.1.3 - es-module-lexer: 1.5.3 - esbuild: 0.21.5 - estree-walker: 3.0.3 - execa: 8.0.1 - fast-glob: 3.3.2 - flattie: 1.1.1 - github-slugger: 2.0.0 - gray-matter: 4.0.3 - html-escaper: 3.0.3 - http-cache-semantics: 4.1.1 - js-yaml: 4.1.0 - kleur: 4.1.5 - magic-string: 0.30.10 - mrmime: 2.0.0 - ora: 8.0.1 - p-limit: 5.0.0 - p-queue: 8.0.1 - path-to-regexp: 6.2.2 - preferred-pm: 3.1.3 - prompts: 2.4.2 - rehype: 13.0.1 - resolve: 1.22.8 - semver: 7.6.2 - shiki: 1.9.0 - string-width: 7.1.0 - strip-ansi: 7.1.0 - tsconfck: 3.1.0(typescript@5.5.2) - unist-util-visit: 5.0.0 - vfile: 6.0.1 - vite: 5.3.1(@types/node@20.14.7) - vitefu: 0.2.5(vite@5.3.1(@types/node@20.14.7)) - which-pm: 2.2.0 - yargs-parser: 21.1.1 - zod: 3.23.8 - zod-to-json-schema: 3.23.1(zod@3.23.8) - optionalDependencies: - sharp: 0.33.4 - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - - typescript - - axobject-query@4.0.0: - dependencies: - dequal: 2.0.3 - - bail@2.0.2: {} - - base-64@1.0.0: {} - - binary-extensions@2.3.0: {} - - boxen@7.1.1: - dependencies: - ansi-align: 3.0.1 - camelcase: 7.0.1 - chalk: 5.3.0 - cli-boxes: 3.0.0 - string-width: 5.1.2 - type-fest: 2.19.0 - widest-line: 4.0.1 - wrap-ansi: 8.1.0 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.23.1: - dependencies: - caniuse-lite: 1.0.30001636 - electron-to-chromium: 1.4.808 - node-releases: 2.0.14 - update-browserslist-db: 1.0.16(browserslist@4.23.1) - - camelcase@7.0.1: {} - - caniuse-lite@1.0.30001636: {} - - ccount@2.0.1: {} - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@5.3.0: {} - - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - character-entities@2.0.2: {} - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - ci-info@4.0.0: {} - - cli-boxes@3.0.0: {} - - cli-cursor@4.0.0: - dependencies: - restore-cursor: 4.0.0 - - cli-spinners@2.9.2: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - clsx@2.1.1: {} - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.3: {} - - color-name@1.1.4: {} - - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - - comma-separated-tokens@2.0.3: {} - - common-ancestor-path@1.0.1: {} - - convert-source-map@2.0.0: {} - - cookie@0.6.0: {} - - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - cssesc@3.0.0: {} - - debug@4.3.5: - dependencies: - ms: 2.1.2 - - decode-named-character-reference@1.0.2: - dependencies: - character-entities: 2.0.2 - - dequal@2.0.3: {} - - detect-libc@2.0.3: - optional: true - - deterministic-object-hash@2.0.2: - dependencies: - base-64: 1.0.0 - - devalue@5.0.0: {} - - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - - diff@5.2.0: {} - - dlv@1.1.3: {} - - dset@3.1.3: {} - - eastasianwidth@0.2.0: {} - - electron-to-chromium@1.4.808: {} - - emmet@2.4.7: - dependencies: - '@emmetio/abbreviation': 2.3.3 - '@emmetio/css-abbreviation': 2.1.8 - - emoji-regex@10.3.0: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - entities@4.5.0: {} - - es-module-lexer@1.5.3: {} - - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - - escalade@3.1.2: {} - - escape-string-regexp@1.0.5: {} - - escape-string-regexp@5.0.0: {} - - esprima@4.0.1: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.5 - - eventemitter3@5.0.1: {} - - execa@8.0.1: - dependencies: - cross-spawn: 7.0.3 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - - extend@3.0.2: {} - - fast-glob@3.3.2: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.7 - - fastq@1.17.1: - dependencies: - reusify: 1.0.4 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - find-yarn-workspace-root2@1.2.16: - dependencies: - micromatch: 4.0.7 - pkg-dir: 4.2.0 - - flattie@1.1.1: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - - get-east-asian-width@1.2.0: {} - - get-stream@8.0.1: {} - - github-slugger@2.0.0: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - globals@11.12.0: {} - - graceful-fs@4.2.11: {} - - gray-matter@4.0.3: - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - - has-flag@3.0.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hast-util-from-html@2.0.1: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - hast-util-from-parse5: 8.0.1 - parse5: 7.1.2 - vfile: 6.0.1 - vfile-message: 4.0.2 - - hast-util-from-parse5@8.0.1: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.2 - devlop: 1.1.0 - hastscript: 8.0.0 - property-information: 6.5.0 - vfile: 6.0.1 - vfile-location: 5.0.2 - web-namespaces: 2.0.1 - - hast-util-is-element@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - hast-util-parse-selector@4.0.0: - dependencies: - '@types/hast': 3.0.4 - - hast-util-raw@9.0.4: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.2 - '@ungap/structured-clone': 1.2.0 - hast-util-from-parse5: 8.0.1 - hast-util-to-parse5: 8.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.0 - parse5: 7.1.2 - unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 - vfile: 6.0.1 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - - hast-util-to-html@9.0.1: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.2 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-raw: 9.0.4 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.0 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - - hast-util-to-parse5@8.0.0: - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - - hast-util-to-text@4.0.2: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.2 - hast-util-is-element: 3.0.0 - unist-util-find-after: 5.0.0 - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - hastscript@8.0.0: - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 4.0.0 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - - html-escaper@3.0.3: {} - - html-void-elements@3.0.0: {} - - http-cache-semantics@4.1.1: {} - - human-signals@5.0.0: {} - - import-meta-resolve@4.1.0: {} - - is-arrayish@0.3.2: - optional: true - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-buffer@2.0.5: {} - - is-core-module@2.14.0: - dependencies: - hasown: 2.0.2 - - is-docker@3.0.0: {} - - is-extendable@0.1.1: {} - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-inside-container@1.0.0: - dependencies: - is-docker: 3.0.0 - - is-interactive@2.0.0: {} - - is-number@7.0.0: {} - - is-plain-obj@4.1.0: {} - - is-stream@3.0.0: {} - - is-unicode-supported@1.3.0: {} - - is-unicode-supported@2.0.0: {} - - is-wsl@3.1.0: - dependencies: - is-inside-container: 1.0.0 - - isexe@2.0.0: {} - - js-tokens@4.0.0: {} - - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsesc@2.5.2: {} - - json5@2.2.3: {} - - jsonc-parser@2.3.1: {} - - kind-of@6.0.3: {} - - kleur@3.0.3: {} - - kleur@4.1.5: {} - - load-yaml-file@0.2.0: - dependencies: - graceful-fs: 4.2.11 - js-yaml: 3.14.1 - pify: 4.0.1 - strip-bom: 3.0.0 - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - log-symbols@6.0.0: - dependencies: - chalk: 5.3.0 - is-unicode-supported: 1.3.0 - - longest-streak@3.1.0: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - magic-string@0.30.10: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - - markdown-table@3.0.3: {} - - mdast-util-definitions@6.0.0: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.2 - unist-util-visit: 5.0.0 - - mdast-util-find-and-replace@3.0.1: - dependencies: - '@types/mdast': 4.0.4 - escape-string-regexp: 5.0.0 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 - - mdast-util-from-markdown@2.0.1: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.2 - decode-named-character-reference: 1.0.2 - devlop: 1.1.0 - mdast-util-to-string: 4.0.0 - micromark: 4.0.0 - micromark-util-decode-numeric-character-reference: 2.0.1 - micromark-util-decode-string: 2.0.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - unist-util-stringify-position: 4.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-autolink-literal@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-find-and-replace: 3.0.1 - micromark-util-character: 2.1.0 - - mdast-util-gfm-footnote@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 - micromark-util-normalize-identifier: 2.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-strikethrough@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-table@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - markdown-table: 3.0.3 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-task-list-item@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm@3.0.0: - dependencies: - mdast-util-from-markdown: 2.0.1 - mdast-util-gfm-autolink-literal: 2.0.0 - mdast-util-gfm-footnote: 2.0.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 - mdast-util-to-markdown: 2.1.0 - transitivePeerDependencies: - - supports-color - - mdast-util-phrasing@4.1.0: - dependencies: - '@types/mdast': 4.0.4 - unist-util-is: 6.0.0 - - mdast-util-to-hast@13.2.0: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.2.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.0 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 - vfile: 6.0.1 - - mdast-util-to-markdown@2.1.0: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.2 - longest-streak: 3.1.0 - mdast-util-phrasing: 4.1.0 - mdast-util-to-string: 4.0.0 - micromark-util-decode-string: 2.0.0 - unist-util-visit: 5.0.0 - zwitch: 2.0.4 - - mdast-util-to-string@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - micromark-core-commonmark@2.0.1: - dependencies: - decode-named-character-reference: 1.0.2 - devlop: 1.1.0 - micromark-factory-destination: 2.0.0 - micromark-factory-label: 2.0.0 - micromark-factory-space: 2.0.0 - micromark-factory-title: 2.0.0 - micromark-factory-whitespace: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-classify-character: 2.0.0 - micromark-util-html-tag-name: 2.0.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-resolve-all: 2.0.0 - micromark-util-subtokenize: 2.0.1 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-extension-gfm-autolink-literal@2.0.0: - dependencies: - micromark-util-character: 2.1.0 - micromark-util-sanitize-uri: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-extension-gfm-footnote@2.0.0: - dependencies: - devlop: 1.1.0 - micromark-core-commonmark: 2.0.1 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-sanitize-uri: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-extension-gfm-strikethrough@2.0.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-classify-character: 2.0.0 - micromark-util-resolve-all: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-extension-gfm-table@2.0.0: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-extension-gfm-tagfilter@2.0.0: - dependencies: - micromark-util-types: 2.0.0 - - micromark-extension-gfm-task-list-item@2.0.1: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-extension-gfm@3.0.0: - dependencies: - micromark-extension-gfm-autolink-literal: 2.0.0 - micromark-extension-gfm-footnote: 2.0.0 - micromark-extension-gfm-strikethrough: 2.0.0 - micromark-extension-gfm-table: 2.0.0 - micromark-extension-gfm-tagfilter: 2.0.0 - micromark-extension-gfm-task-list-item: 2.0.1 - micromark-util-combine-extensions: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-factory-destination@2.0.0: - dependencies: - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-factory-label@2.0.0: - dependencies: - devlop: 1.1.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-factory-space@2.0.0: - dependencies: - micromark-util-character: 2.1.0 - micromark-util-types: 2.0.0 - - micromark-factory-title@2.0.0: - dependencies: - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-factory-whitespace@2.0.0: - dependencies: - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-util-character@2.1.0: - dependencies: - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-util-chunked@2.0.0: - dependencies: - micromark-util-symbol: 2.0.0 - - micromark-util-classify-character@2.0.0: - dependencies: - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-util-combine-extensions@2.0.0: - dependencies: - micromark-util-chunked: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-util-decode-numeric-character-reference@2.0.1: - dependencies: - micromark-util-symbol: 2.0.0 - - micromark-util-decode-string@2.0.0: - dependencies: - decode-named-character-reference: 1.0.2 - micromark-util-character: 2.1.0 - micromark-util-decode-numeric-character-reference: 2.0.1 - micromark-util-symbol: 2.0.0 - - micromark-util-encode@2.0.0: {} - - micromark-util-html-tag-name@2.0.0: {} - - micromark-util-normalize-identifier@2.0.0: - dependencies: - micromark-util-symbol: 2.0.0 - - micromark-util-resolve-all@2.0.0: - dependencies: - micromark-util-types: 2.0.0 - - micromark-util-sanitize-uri@2.0.0: - dependencies: - micromark-util-character: 2.1.0 - micromark-util-encode: 2.0.0 - micromark-util-symbol: 2.0.0 - - micromark-util-subtokenize@2.0.1: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - - micromark-util-symbol@2.0.0: {} - - micromark-util-types@2.0.0: {} - - micromark@4.0.0: - dependencies: - '@types/debug': 4.1.12 - debug: 4.3.5 - decode-named-character-reference: 1.0.2 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.1 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-combine-extensions: 2.0.0 - micromark-util-decode-numeric-character-reference: 2.0.1 - micromark-util-encode: 2.0.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-resolve-all: 2.0.0 - micromark-util-sanitize-uri: 2.0.0 - micromark-util-subtokenize: 2.0.1 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - transitivePeerDependencies: - - supports-color - - micromatch@4.0.7: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mimic-fn@2.1.0: {} - - mimic-fn@4.0.0: {} - - mrmime@2.0.0: {} - - ms@2.1.2: {} - - muggle-string@0.4.1: {} - - nanoid@3.3.7: {} - - nlcst-to-string@3.1.1: - dependencies: - '@types/nlcst': 1.0.4 - - node-releases@2.0.14: {} - - normalize-path@3.0.0: {} - - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - - obuf@1.1.2: {} - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - onetime@6.0.0: - dependencies: - mimic-fn: 4.0.0 - - ora@8.0.1: - dependencies: - chalk: 5.3.0 - cli-cursor: 4.0.0 - cli-spinners: 2.9.2 - is-interactive: 2.0.0 - is-unicode-supported: 2.0.0 - log-symbols: 6.0.0 - stdin-discarder: 0.2.2 - string-width: 7.1.0 - strip-ansi: 7.1.0 - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-limit@5.0.0: - dependencies: - yocto-queue: 1.0.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - p-queue@8.0.1: - dependencies: - eventemitter3: 5.0.1 - p-timeout: 6.1.2 - - p-timeout@6.1.2: {} - - p-try@2.2.0: {} - - parse-latin@5.0.1: - dependencies: - nlcst-to-string: 3.1.1 - unist-util-modify-children: 3.1.1 - unist-util-visit-children: 2.0.2 - - parse5@7.1.2: - dependencies: - entities: 4.5.0 - - path-browserify@1.0.1: {} - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - path-key@4.0.0: {} - - path-parse@1.0.7: {} - - path-to-regexp@6.2.2: {} - - pg-cloudflare@1.1.1: - optional: true - - pg-connection-string@2.6.4: {} - - pg-int8@1.0.1: {} - - pg-numeric@1.0.2: {} - - pg-pool@3.6.2(pg@8.12.0): - dependencies: - pg: 8.12.0 - - pg-protocol@1.6.1: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.0 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - pg-types@4.0.2: - dependencies: - pg-int8: 1.0.1 - pg-numeric: 1.0.2 - postgres-array: 3.0.2 - postgres-bytea: 3.0.0 - postgres-date: 2.1.0 - postgres-interval: 3.0.0 - postgres-range: 1.1.4 - - pg@8.12.0: - dependencies: - pg-connection-string: 2.6.4 - pg-pool: 3.6.2(pg@8.12.0) - pg-protocol: 1.6.1 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.1.1 - - pgpass@1.0.5: - dependencies: - split2: 4.2.0 - - picocolors@1.0.1: {} - - picomatch@2.3.1: {} - - pify@4.0.1: {} - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - - postcss@8.4.38: - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 - - postgres-array@2.0.0: {} - - postgres-array@3.0.2: {} - - postgres-bytea@1.0.0: {} - - postgres-bytea@3.0.0: - dependencies: - obuf: 1.1.2 - - postgres-date@1.0.7: {} - - postgres-date@2.1.0: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - - postgres-interval@3.0.0: {} - - postgres-range@1.1.4: {} - - preferred-pm@3.1.3: - dependencies: - find-up: 5.0.0 - find-yarn-workspace-root2: 1.2.16 - path-exists: 4.0.0 - which-pm: 2.0.0 - - prismjs@1.29.0: {} - - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - property-information@6.5.0: {} - - queue-microtask@1.2.3: {} - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - rehype-parse@9.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-from-html: 2.0.1 - unified: 11.0.5 - - rehype-raw@7.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-raw: 9.0.4 - vfile: 6.0.1 - - rehype-stringify@10.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.1 - unified: 11.0.5 - - rehype@13.0.1: - dependencies: - '@types/hast': 3.0.4 - rehype-parse: 9.0.0 - rehype-stringify: 10.0.0 - unified: 11.0.5 - - remark-gfm@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-gfm: 3.0.0 - micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-parse@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.1 - micromark-util-types: 2.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-rehype@11.1.0: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.0 - unified: 11.0.5 - vfile: 6.0.1 - - remark-smartypants@2.1.0: - dependencies: - retext: 8.1.0 - retext-smartypants: 5.2.0 - unist-util-visit: 5.0.0 - - remark-stringify@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-to-markdown: 2.1.0 - unified: 11.0.5 - - request-light@0.7.0: {} - - require-directory@2.1.1: {} - - resolve@1.22.8: - dependencies: - is-core-module: 2.14.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - restore-cursor@4.0.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - - retext-latin@3.1.0: - dependencies: - '@types/nlcst': 1.0.4 - parse-latin: 5.0.1 - unherit: 3.0.1 - unified: 10.1.2 - - retext-smartypants@5.2.0: - dependencies: - '@types/nlcst': 1.0.4 - nlcst-to-string: 3.1.1 - unified: 10.1.2 - unist-util-visit: 4.1.2 - - retext-stringify@3.1.0: - dependencies: - '@types/nlcst': 1.0.4 - nlcst-to-string: 3.1.1 - unified: 10.1.2 - - retext@8.1.0: - dependencies: - '@types/nlcst': 1.0.4 - retext-latin: 3.1.0 - retext-stringify: 3.1.0 - unified: 10.1.2 - - reusify@1.0.4: {} - - rollup@4.18.0: - dependencies: - '@types/estree': 1.0.5 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.18.0 - '@rollup/rollup-android-arm64': 4.18.0 - '@rollup/rollup-darwin-arm64': 4.18.0 - '@rollup/rollup-darwin-x64': 4.18.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.18.0 - '@rollup/rollup-linux-arm-musleabihf': 4.18.0 - '@rollup/rollup-linux-arm64-gnu': 4.18.0 - '@rollup/rollup-linux-arm64-musl': 4.18.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.18.0 - '@rollup/rollup-linux-riscv64-gnu': 4.18.0 - '@rollup/rollup-linux-s390x-gnu': 4.18.0 - '@rollup/rollup-linux-x64-gnu': 4.18.0 - '@rollup/rollup-linux-x64-musl': 4.18.0 - '@rollup/rollup-win32-arm64-msvc': 4.18.0 - '@rollup/rollup-win32-ia32-msvc': 4.18.0 - '@rollup/rollup-win32-x64-msvc': 4.18.0 - fsevents: 2.3.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - section-matter@1.0.0: - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - - semver@6.3.1: {} - - semver@7.6.2: {} - - sharp@0.33.4: - dependencies: - color: 4.2.3 - detect-libc: 2.0.3 - semver: 7.6.2 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.4 - '@img/sharp-darwin-x64': 0.33.4 - '@img/sharp-libvips-darwin-arm64': 1.0.2 - '@img/sharp-libvips-darwin-x64': 1.0.2 - '@img/sharp-libvips-linux-arm': 1.0.2 - '@img/sharp-libvips-linux-arm64': 1.0.2 - '@img/sharp-libvips-linux-s390x': 1.0.2 - '@img/sharp-libvips-linux-x64': 1.0.2 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 - '@img/sharp-libvips-linuxmusl-x64': 1.0.2 - '@img/sharp-linux-arm': 0.33.4 - '@img/sharp-linux-arm64': 0.33.4 - '@img/sharp-linux-s390x': 0.33.4 - '@img/sharp-linux-x64': 0.33.4 - '@img/sharp-linuxmusl-arm64': 0.33.4 - '@img/sharp-linuxmusl-x64': 0.33.4 - '@img/sharp-wasm32': 0.33.4 - '@img/sharp-win32-ia32': 0.33.4 - '@img/sharp-win32-x64': 0.33.4 - optional: true - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - shiki@1.9.0: - dependencies: - '@shikijs/core': 1.9.0 - - signal-exit@3.0.7: {} - - signal-exit@4.1.0: {} - - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - optional: true - - sisteransi@1.0.5: {} - - source-map-js@1.2.0: {} - - space-separated-tokens@2.0.2: {} - - split2@4.2.0: {} - - sprintf-js@1.0.3: {} - - stdin-discarder@0.2.2: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - - string-width@7.1.0: - dependencies: - emoji-regex: 10.3.0 - get-east-asian-width: 1.2.0 - strip-ansi: 7.1.0 - - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.0.1 - - strip-bom-string@1.0.0: {} - - strip-bom@3.0.0: {} - - strip-final-newline@3.0.0: {} - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - to-fast-properties@2.0.0: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - trim-lines@3.0.1: {} - - trough@2.2.0: {} - - tsconfck@3.1.0(typescript@5.5.2): - optionalDependencies: - typescript: 5.5.2 - - tslib@2.6.3: - optional: true - - type-fest@2.19.0: {} - - typesafe-path@0.2.2: {} - - typescript-auto-import-cache@0.3.3: - dependencies: - semver: 7.6.2 - - typescript@5.5.2: {} - - undici-types@5.26.5: {} - - unherit@3.0.1: {} - - unified@10.1.2: - dependencies: - '@types/unist': 2.0.10 - bail: 2.0.2 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 5.3.7 - - unified@11.0.5: - dependencies: - '@types/unist': 3.0.2 - bail: 2.0.2 - devlop: 1.1.0 - extend: 3.0.2 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 6.0.1 - - unist-util-find-after@5.0.0: - dependencies: - '@types/unist': 3.0.2 - unist-util-is: 6.0.0 - - unist-util-is@5.2.1: - dependencies: - '@types/unist': 2.0.10 - - unist-util-is@6.0.0: - dependencies: - '@types/unist': 3.0.2 - - unist-util-modify-children@3.1.1: - dependencies: - '@types/unist': 2.0.10 - array-iterate: 2.0.1 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.2 - - unist-util-remove-position@5.0.0: - dependencies: - '@types/unist': 3.0.2 - unist-util-visit: 5.0.0 - - unist-util-stringify-position@3.0.3: - dependencies: - '@types/unist': 2.0.10 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.2 - - unist-util-visit-children@2.0.2: - dependencies: - '@types/unist': 2.0.10 - - unist-util-visit-parents@5.1.3: - dependencies: - '@types/unist': 2.0.10 - unist-util-is: 5.2.1 - - unist-util-visit-parents@6.0.1: - dependencies: - '@types/unist': 3.0.2 - unist-util-is: 6.0.0 - - unist-util-visit@4.1.2: - dependencies: - '@types/unist': 2.0.10 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - - unist-util-visit@5.0.0: - dependencies: - '@types/unist': 3.0.2 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 - - update-browserslist-db@1.0.16(browserslist@4.23.1): - dependencies: - browserslist: 4.23.1 - escalade: 3.1.2 - picocolors: 1.0.1 - - vfile-location@5.0.2: - dependencies: - '@types/unist': 3.0.2 - vfile: 6.0.1 - - vfile-message@3.1.4: - dependencies: - '@types/unist': 2.0.10 - unist-util-stringify-position: 3.0.3 - - vfile-message@4.0.2: - dependencies: - '@types/unist': 3.0.2 - unist-util-stringify-position: 4.0.0 - - vfile@5.3.7: - dependencies: - '@types/unist': 2.0.10 - is-buffer: 2.0.5 - unist-util-stringify-position: 3.0.3 - vfile-message: 3.1.4 - - vfile@6.0.1: - dependencies: - '@types/unist': 3.0.2 - unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.2 - - vite@5.3.1(@types/node@20.14.7): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.38 - rollup: 4.18.0 - optionalDependencies: - '@types/node': 20.14.7 - fsevents: 2.3.3 - - vitefu@0.2.5(vite@5.3.1(@types/node@20.14.7)): - optionalDependencies: - vite: 5.3.1(@types/node@20.14.7) - - volar-service-css@0.0.45(@volar/language-service@2.2.5): - dependencies: - vscode-css-languageservice: 6.2.14 - vscode-languageserver-textdocument: 1.0.11 - vscode-uri: 3.0.8 - optionalDependencies: - '@volar/language-service': 2.2.5 - - volar-service-emmet@0.0.45(@volar/language-service@2.2.5): - dependencies: - '@emmetio/css-parser': 0.4.0 - '@emmetio/html-matcher': 1.3.0 - '@vscode/emmet-helper': 2.9.3 - optionalDependencies: - '@volar/language-service': 2.2.5 - - volar-service-html@0.0.45(@volar/language-service@2.2.5): - dependencies: - vscode-html-languageservice: '@johnsoncodehk/vscode-html-languageservice@5.2.0-34a5462' - vscode-languageserver-textdocument: 1.0.11 - vscode-uri: 3.0.8 - optionalDependencies: - '@volar/language-service': 2.2.5 - - volar-service-prettier@0.0.45(@volar/language-service@2.2.5): - dependencies: - vscode-uri: 3.0.8 - optionalDependencies: - '@volar/language-service': 2.2.5 - - volar-service-typescript-twoslash-queries@0.0.45(@volar/language-service@2.2.5): - optionalDependencies: - '@volar/language-service': 2.2.5 - - volar-service-typescript@0.0.45(@volar/language-service@2.2.5): - dependencies: - path-browserify: 1.0.1 - semver: 7.6.2 - typescript-auto-import-cache: 0.3.3 - vscode-languageserver-textdocument: 1.0.11 - vscode-nls: 5.2.0 - optionalDependencies: - '@volar/language-service': 2.2.5 - - vscode-css-languageservice@6.2.14: - dependencies: - '@vscode/l10n': 0.0.18 - vscode-languageserver-textdocument: 1.0.11 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.0.8 - - vscode-html-languageservice@5.2.0: - dependencies: - '@vscode/l10n': 0.0.18 - vscode-languageserver-textdocument: 1.0.11 - vscode-languageserver-types: 3.17.5 - vscode-uri: 3.0.8 - - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-textdocument@1.0.11: {} - - vscode-languageserver-types@3.17.5: {} - - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 - - vscode-nls@5.2.0: {} - - vscode-uri@2.1.2: {} - - vscode-uri@3.0.8: {} - - web-namespaces@2.0.1: {} - - which-pm-runs@1.1.0: {} - - which-pm@2.0.0: - dependencies: - load-yaml-file: 0.2.0 - path-exists: 4.0.0 - - which-pm@2.2.0: - dependencies: - load-yaml-file: 0.2.0 - path-exists: 4.0.0 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - widest-line@4.0.1: - dependencies: - string-width: 5.1.2 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - - xtend@4.0.2: {} - - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.1.2 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yocto-queue@0.1.0: {} - - yocto-queue@1.0.0: {} - - zod-to-json-schema@3.23.1(zod@3.23.8): - dependencies: - zod: 3.23.8 - - zod@3.23.8: {} - - zwitch@2.0.4: {} diff --git a/indexer/postgres/graphql-proxy/public/favicon.svg b/indexer/postgres/graphql-proxy/public/favicon.svg deleted file mode 100644 index f157bd1c5e28..000000000000 --- a/indexer/postgres/graphql-proxy/public/favicon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/indexer/postgres/graphql-proxy/src/env.d.ts b/indexer/postgres/graphql-proxy/src/env.d.ts deleted file mode 100644 index f964fe0cffd8..000000000000 --- a/indexer/postgres/graphql-proxy/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/indexer/postgres/graphql-proxy/src/pages/graphiql.astro b/indexer/postgres/graphql-proxy/src/pages/graphiql.astro deleted file mode 100644 index 02674b666423..000000000000 --- a/indexer/postgres/graphql-proxy/src/pages/graphiql.astro +++ /dev/null @@ -1,81 +0,0 @@ - - - - - GraphiQL - - - - - - - - - - - - - - -
Loading...
- - - \ No newline at end of file diff --git a/indexer/postgres/graphql-proxy/src/pages/graphql.ts b/indexer/postgres/graphql-proxy/src/pages/graphql.ts deleted file mode 100644 index e9791859d4ef..000000000000 --- a/indexer/postgres/graphql-proxy/src/pages/graphql.ts +++ /dev/null @@ -1,47 +0,0 @@ -import pg from 'pg' - -const {Client} = pg -const client = new Client({ - connectionString: import.meta.env.DATABASE_URL, -}) -await client.connect() - - -export async function GET({params, request}) { - const {query, variables, operationName} = params; - - return graphql(query, variables, operationName); -} - -export async function POST({params, request}) { - const {query, operationName, variables} = await request.json(); - return graphql(query, variables, operationName); -} - -async function graphql(query, variables, operationName) { - try { - const res = await client.query( - 'select graphql.resolve($1, $2, $3);', - [query, variables, operationName] - ); - - return new Response( - JSON.stringify(res.rows[0].resolve), - { - headers: { - 'Content-Type': 'application/json', - } - } - ) - } catch (e) { - return new Response( - JSON.stringify({errors: [e.message]}), - { - status: 500, - headers: { - 'Content-Type': 'application/json', - } - } - ) - } -} diff --git a/indexer/postgres/graphql-proxy/src/pages/index.astro b/indexer/postgres/graphql-proxy/src/pages/index.astro deleted file mode 100644 index 240be0a56aca..000000000000 --- a/indexer/postgres/graphql-proxy/src/pages/index.astro +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - OK - - diff --git a/indexer/postgres/graphql-proxy/tsconfig.json b/indexer/postgres/graphql-proxy/tsconfig.json deleted file mode 100644 index 77da9dd00982..000000000000 --- a/indexer/postgres/graphql-proxy/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "astro/tsconfigs/strict" -} \ No newline at end of file From 66beb7b119fcc55a09d2e59bd6e0c59ef96823de Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 19:23:09 +0200 Subject: [PATCH 35/79] cleanup --- indexer/postgres/graphql.go | 97 -------------------------- indexer/postgres/testing/app/main.go | 100 --------------------------- 2 files changed, 197 deletions(-) delete mode 100644 indexer/postgres/graphql.go delete mode 100644 indexer/postgres/testing/app/main.go diff --git a/indexer/postgres/graphql.go b/indexer/postgres/graphql.go deleted file mode 100644 index c353c19f137d..000000000000 --- a/indexer/postgres/graphql.go +++ /dev/null @@ -1,97 +0,0 @@ -package postgres - -import ( - "database/sql" - "encoding/json" - "net/http" -) - -type graphqlHandler struct { - conn *sql.DB -} - -func NewGraphQLHandler(conn *sql.DB) http.Handler { - return &graphqlHandler{conn: conn} -} - -var _ http.Handler = &graphqlHandler{} - -type graphqlRequest struct { - Query string `json:"query"` - OperationName string `json:"operationName"` - Variables string `json:"variables"` -} - -type graphqlResponse struct { - Data json.RawMessage `json:"data"` - Errors []error `json:"errors,omitempty"` -} - -func (g graphqlHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - switch request.Method { - case http.MethodGet: - g.handleGet(writer, request) - case http.MethodPost: - g.handlePost(writer, request) - default: - http.Error(writer, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -func (g graphqlHandler) handlePost(writer http.ResponseWriter, request *http.Request) { - var req graphqlRequest - err := json.NewDecoder(request.Body).Decode(&req) - if err != nil { - http.Error(writer, err.Error(), http.StatusBadRequest) - return - } - - g.handle(writer, request, &req) -} - -func (g graphqlHandler) handleGet(writer http.ResponseWriter, request *http.Request) { - var gqlReq graphqlRequest - gqlReq.Query = request.URL.Query().Get("query") - gqlReq.OperationName = request.URL.Query().Get("operationName") - gqlReq.Variables = request.URL.Query().Get("variables") - - g.handle(writer, request, &gqlReq) -} - -func (g graphqlHandler) handle(writer http.ResponseWriter, request *http.Request, gqlReq *graphqlRequest) { - rows, err := g.conn.QueryContext( - request.Context(), - `select graphql.resolve($1, $2, $3);`, - gqlReq.Query, - gqlReq.OperationName, - gqlReq.Variables, - ) - - if err != nil { - http.Error(writer, err.Error(), http.StatusInternalServerError) - return - } - defer func(rows *sql.Rows) { - err := rows.Close() - if err != nil { - - } - }(rows) - - var data json.RawMessage - for rows.Next() { - err = rows.Scan(&data) - if err != nil { - http.Error(writer, err.Error(), http.StatusInternalServerError) - return - } - } - - resp := graphqlResponse{Data: data} - err = json.NewEncoder(writer).Encode(resp) - if err != nil { - http.Error(writer, err.Error(), http.StatusInternalServerError) - } - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(http.StatusOK) -} diff --git a/indexer/postgres/testing/app/main.go b/indexer/postgres/testing/app/main.go deleted file mode 100644 index 9569c0d53187..000000000000 --- a/indexer/postgres/testing/app/main.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "context" - "database/sql" - "os" - - "cosmossdk.io/log" - embeddedpostgres "github.com/fergusstrange/embedded-postgres" - "github.com/hashicorp/consul/sdk/freeport" - _ "github.com/jackc/pgx/v5/stdlib" - - "cosmossdk.io/indexer/postgres" - "cosmossdk.io/schema/indexing" - indexertesting "cosmossdk.io/schema/testing" - appdatatest "cosmossdk.io/schema/testing/appdata" - "cosmossdk.io/schema/testing/statesim" -) - -func start() error { - dbUrl, found := os.LookupEnv("DATABASE_URL") - if !found { - tempDir, err := os.MkdirTemp("", "postgres-indexer-test") - if err != nil { - return err - } - defer func(path string) { - err := os.RemoveAll(path) - if err != nil { - panic(err) - } - }(tempDir) - - dbPort := freeport.MustTake(1)[0] - pgConfig := embeddedpostgres.DefaultConfig(). - Port(uint32(dbPort)). - DataPath(tempDir) - - dbUrl = pgConfig.GetConnectionURL() - pg := embeddedpostgres.NewDatabase(pgConfig) - err = pg.Start() - if err != nil { - return err - } - defer func(pg *embeddedpostgres.EmbeddedPostgres) { - err := pg.Stop() - if err != nil { - panic(err) - } - }(pg) - } - - db, err := sql.Open("pgx", dbUrl) - if err != nil { - return err - } - - indexer, err := postgres.NewIndexer(db, postgres.Options{ - Logger: log.NewLogger(os.Stdout), - }) - if err != nil { - return err - } - - res, err := indexer.Initialize(context.Background(), indexing.InitializationData{}) - if err != nil { - return err - } - - fixture := appdatatest.NewSimulator(appdatatest.SimulatorOptions{ - Listener: res.Listener, - AppSchema: indexertesting.ExampleAppSchema, - StateSimOptions: statesim.Options{}, - }) - - err = fixture.Initialize() - if err != nil { - return err - } - - blockDataGen := fixture.BlockDataGenN(1000) - i := 0 - for { - blockData := blockDataGen.Example(i) - err = fixture.ProcessBlockData(blockData) - if err != nil { - return err - } - i++ - } - - return nil -} - -func main() { - err := start() - if err != nil { - panic(err) - } -} From e2ee268eb5683ecc8cdc9ac9a4a817cec61fbfbe Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 19:25:11 +0200 Subject: [PATCH 36/79] cleanup --- schema/object_type.go | 2 -- schema/unique.go | 5 ----- 2 files changed, 7 deletions(-) delete mode 100644 schema/unique.go diff --git a/schema/object_type.go b/schema/object_type.go index 31aaf71aa6be..89306a07ccb9 100644 --- a/schema/object_type.go +++ b/schema/object_type.go @@ -25,8 +25,6 @@ type ObjectType struct { // though it is still valid in order to save space. Indexers will want to have // the option of retaining such data and distinguishing from other "true" deletions. RetainDeletions bool - - UniqueConstraints []UniqueConstraint } // Validate validates the object type. diff --git a/schema/unique.go b/schema/unique.go deleted file mode 100644 index 9d6db7d88700..000000000000 --- a/schema/unique.go +++ /dev/null @@ -1,5 +0,0 @@ -package schema - -type UniqueConstraint struct { - FieldNames []string -} From a6cf9aa9c69f5a55fff98a1563162d704596e1c7 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 19:28:43 +0200 Subject: [PATCH 37/79] cleanup --- schema/decoding/README.md | 42 --------------- schema/decoding/middleware.go | 87 -------------------------------- schema/decoding/resolver.go | 71 -------------------------- schema/decoding/sync.go | 8 --- schema/indexing/logger.go | 1 - schema/indexing/manager.go | 70 ------------------------- schema/testing/example_schema.go | 22 -------- 7 files changed, 301 deletions(-) delete mode 100644 schema/decoding/README.md delete mode 100644 schema/decoding/middleware.go delete mode 100644 schema/decoding/resolver.go delete mode 100644 schema/decoding/sync.go delete mode 100644 schema/indexing/logger.go delete mode 100644 schema/indexing/manager.go diff --git a/schema/decoding/README.md b/schema/decoding/README.md deleted file mode 100644 index 21b5dd2fd2a5..000000000000 --- a/schema/decoding/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Indexer Base - -The indexer base module is designed to provide a stable, zero-dependency base layer for the built-in indexer functionality. Packages that integrate with the indexer should feel free to depend on this package without fear of any external dependencies being pulled in. - -The basic types for specifying index sources, targets and decoders are provided here. An indexing source should accept a `Listener` instance and invoke the provided callbacks in the correct order. An indexer should provide a `Listener` instance and perform indexing operations based on the data passed to its callbacks. A module that exposes logical updates in the form of `ObjectUpdate`s should implement the `IndexableModule` interface. - -## `Listener` Callback Order - -`Listener` callbacks should be called in this order - -```mermaid -sequenceDiagram - actor Source - actor Manager - participant Indexer - Source -->> Manager: InitializeModuleSchema - Manager ->> Indexer: InitializeModuleSchema - Source ->> Manager: Initialize - Manager ->> Indexer: Initialize - loop Block - Source ->> Manager: StartBlock - Manager ->> Indexer: StartBlock - Source -->> Manager: OnBlockHeader - Manager -->> Indexer: OnBlockHeader - Source -->> Manager: OnTx - Manager -->> Indexer: OnTx - Source -->> Manager: OnEvent - Manager -->> Indexer: OnEvent - Source -->> Manager: OnKVPair - Manager -->> Indexer: OnKVPair - Source -->> Manager: OnObjectUpdate - Manager -->> Indexer: OnObjectUpdate - Source ->> Manager: Commit - Manager ->> Indexer: Commit - end -``` - -`InitializeModuleSchema` should be called at most once for every module with logical data and all calls to should happen even before `Initialize` is called. After that `Initialize` MUST be called before any other method and should only be invoked once. - -Sources will generally only call `InitializeModuleSchema` and `OnObjectUpdate` if they have native logical decoding capabilities. Usually, the indexer framework will provide this functionality based on `OnKVPair` data and `IndexableModule` implementations. - -`StartBlock` and `OnBlockHeader` should be called only once at the beginning of a block, and `Commit` should be called only once at the end of a block. The `OnTx`, `OnEvent`, `OnKVPair` and `OnObjectUpdate` must be called after `OnBlockHeader`, may be called multiple times within a block and indexers should not assume that the order is logical unless `InitializationData.HasEventAlignedWrites` is true. diff --git a/schema/decoding/middleware.go b/schema/decoding/middleware.go deleted file mode 100644 index 3364a3bc9728..000000000000 --- a/schema/decoding/middleware.go +++ /dev/null @@ -1,87 +0,0 @@ -package decoding - -import ( - "fmt" - - "cosmossdk.io/schema" - "cosmossdk.io/schema/appdata" - "cosmossdk.io/schema/log" -) - -type Options struct { - DecoderResolver DecoderResolver - - // SyncSource is the source that will be used do initial indexing of modules with pre-existing - // state. It is optional, but if it is not provided, indexing can only be starting when a node - // is synced from genesis. - SyncSource SyncSource - Logger log.Logger -} - -func Middleware(target appdata.Listener, opts Options) (appdata.Listener, error) { - initializeModuleData := target.InitializeModuleData - onKVPair := target.OnKVPair - - moduleCodecs := map[string]schema.ModuleCodec{} - if opts.DecoderResolver != nil { - err := opts.DecoderResolver.Iterate(func(moduleName string, codec schema.ModuleCodec) error { - opts.Logger.Info("Initializing module schema", "moduleName", moduleName) - - if _, ok := moduleCodecs[moduleName]; ok { - return fmt.Errorf("module %s already initialized", moduleName) - } - - if err := codec.Schema.Validate(); err != nil { - return fmt.Errorf("error validating schema for module %s: %w", moduleName, err) - } - - moduleCodecs[moduleName] = codec - if initializeModuleData != nil { - return initializeModuleData(appdata.ModuleInitializationData{ - ModuleName: moduleName, - Schema: codec.Schema, - }) - } - return nil - }) - if err != nil { - return appdata.Listener{}, err - } - } - - // TODO: catch-up sync - - target.OnKVPair = func(data appdata.KVPairData) error { - if onKVPair != nil { - return onKVPair(data) - } - - if target.OnObjectUpdate != nil { - for _, kvUpdate := range data.Updates { - codec, ok := moduleCodecs[kvUpdate.ModuleName] - if !ok { - // TODO handle discovering a new module - return nil - } - - updates, err := codec.KVDecoder(kvUpdate.Update) - if err != nil { - return err - } - - if !ok { - return nil - } - - return target.OnObjectUpdate(appdata.ObjectUpdateData{ - ModuleName: kvUpdate.ModuleName, - Updates: updates, - }) - } - } - - return nil - } - - return target, nil -} diff --git a/schema/decoding/resolver.go b/schema/decoding/resolver.go deleted file mode 100644 index ef2c720eb411..000000000000 --- a/schema/decoding/resolver.go +++ /dev/null @@ -1,71 +0,0 @@ -package decoding - -import ( - "sort" - - "cosmossdk.io/schema" -) - -type DecoderResolver interface { - // Iterate iterates over all module decoders which should be initialized at startup. - Iterate(func(string, schema.ModuleCodec) error) error - - // LookupDecoder allows for resolving decoders dynamically. For instance, some module-like - // things may come into existence dynamically (like x/accounts or EVM or WASM contracts). - // The first time the manager sees one of these appearing in KV-store writes, it will - // lookup a decoder for it and cache it for future use. The manager will also perform - // a catch-up sync before passing any new writes to ensure that all historical state has - // been synced if there is any This check will only happen the first time a module is seen - // by the manager in a given process (a process restart will cause this check to happen again). - LookupDecoder(moduleName string) (decoder schema.ModuleCodec, found bool, err error) -} - -type moduleSetDecoderResolver struct { - moduleSet map[string]interface{} -} - -// ModuleSetDecoderResolver returns DecoderResolver that will discover modules implementing -// DecodeableModule in the provided module set. -func ModuleSetDecoderResolver(moduleSet map[string]interface{}) DecoderResolver { - return &moduleSetDecoderResolver{ - moduleSet: moduleSet, - } -} - -func (a moduleSetDecoderResolver) Iterate(f func(string, schema.ModuleCodec) error) error { - keys := make([]string, 0, len(a.moduleSet)) - for k := range a.moduleSet { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - module := a.moduleSet[k] - dm, ok := module.(schema.HasModuleCodec) - if ok { - decoder, err := dm.ModuleCodec() - if err != nil { - return err - } - err = f(k, decoder) - if err != nil { - return err - } - } - } - return nil -} - -func (a moduleSetDecoderResolver) LookupDecoder(moduleName string) (schema.ModuleCodec, bool, error) { - mod, ok := a.moduleSet[moduleName] - if !ok { - return schema.ModuleCodec{}, false, nil - } - - dm, ok := mod.(schema.HasModuleCodec) - if !ok { - return schema.ModuleCodec{}, false, nil - } - - decoder, err := dm.ModuleCodec() - return decoder, true, err -} diff --git a/schema/decoding/sync.go b/schema/decoding/sync.go deleted file mode 100644 index 1bcd56fa0f89..000000000000 --- a/schema/decoding/sync.go +++ /dev/null @@ -1,8 +0,0 @@ -package decoding - -// SyncSource is an interface that allows indexers to start indexing modules with pre-existing state. -type SyncSource interface { - - // IterateAllKVPairs iterates over all key-value pairs for a given module. - IterateAllKVPairs(moduleName string, fn func(key, value []byte) error) error -} diff --git a/schema/indexing/logger.go b/schema/indexing/logger.go deleted file mode 100644 index 900a8d89cebe..000000000000 --- a/schema/indexing/logger.go +++ /dev/null @@ -1 +0,0 @@ -package indexing diff --git a/schema/indexing/manager.go b/schema/indexing/manager.go deleted file mode 100644 index c858fbdc4ee8..000000000000 --- a/schema/indexing/manager.go +++ /dev/null @@ -1,70 +0,0 @@ -package indexing - -import ( - "context" - "fmt" - - "cosmossdk.io/schema/appdata" - "cosmossdk.io/schema/decoding" - "cosmossdk.io/schema/log" -) - -type Options struct { - Context context.Context - Options map[string]interface{} - Resolver decoding.DecoderResolver - SyncSource decoding.SyncSource - Logger log.Logger -} - -func Start(opts Options) (appdata.Listener, error) { - if opts.Logger == nil { - opts.Logger = log.NoopLogger{} - } - - opts.Logger.Info("Starting Indexer Manager") - - resources := IndexerResources{Logger: opts.Logger} - - var indexers []appdata.Listener - ctx := opts.Context - if ctx == nil { - ctx = context.Background() - } - - for indexerName, factory := range indexerRegistry { - indexerOpts, ok := opts.Options[indexerName] - if !ok { - continue - } - - if opts.Logger != nil { - opts.Logger.Info(fmt.Sprintf("Starting Indexer %s", indexerName), "options", indexerOpts) - } - - optsMap, ok := indexerOpts.(map[string]interface{}) - if !ok { - return appdata.Listener{}, fmt.Errorf("invalid indexer options type %T for %s, expected a map", indexerOpts, indexerName) - } - - indexer, err := factory(optsMap, resources) - if err != nil { - return appdata.Listener{}, fmt.Errorf("failed to create indexer %s: %w", indexerName, err) - } - - res, err := indexer.Initialize(ctx, InitializationData{}) - if err != nil { - return appdata.Listener{}, fmt.Errorf("failed to initialize indexer %s: %w", indexerName, err) - } - - indexers = append(indexers, res.Listener) - - // TODO handle last block persisted - } - - return decoding.Middleware(appdata.AsyncListenerMux(indexers, 1024, ctx.Done()), decoding.Options{ - DecoderResolver: opts.Resolver, - SyncSource: opts.SyncSource, - Logger: opts.Logger, - }) -} diff --git a/schema/testing/example_schema.go b/schema/testing/example_schema.go index 77d1e66c6879..df2773ea9282 100644 --- a/schema/testing/example_schema.go +++ b/schema/testing/example_schema.go @@ -126,28 +126,6 @@ var ExampleAppSchema = map[string]schema.ModuleSchema{ }, RetainDeletions: true, }, - { - Name: "UniqueConstraint", - KeyFields: []schema.Field{ - { - Name: "Key", - Kind: schema.StringKind, - }, - }, - ValueFields: []schema.Field{ - { - Name: "Value1", - Kind: schema.Int32Kind, - }, - { - Name: "Value2", - Kind: schema.BytesKind, - }, - }, - UniqueConstraints: []schema.UniqueConstraint{ - {[]string{"Value1"}}, - }, - }, }, }, } From 82114b074fa67b0fef4823b58847dd44ebbecf85 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 19:31:05 +0200 Subject: [PATCH 38/79] refactoring --- indexer/postgres/testing/postgres_test.go | 4 +- schema/testing/appdata/json.go | 40 ------------------- .../{appdata => appdatasim}/app_data.go | 2 +- .../{appdata => appdatasim}/app_data_test.go | 2 +- .../testdata/app_sim_example_schema.txt | 0 .../{appdata => appdatasim}/write_listener.go | 2 +- 6 files changed, 5 insertions(+), 45 deletions(-) delete mode 100644 schema/testing/appdata/json.go rename schema/testing/{appdata => appdatasim}/app_data.go (99%) rename schema/testing/{appdata => appdatasim}/app_data_test.go (96%) rename schema/testing/{appdata => appdatasim}/testdata/app_sim_example_schema.txt (100%) rename schema/testing/{appdata => appdatasim}/write_listener.go (97%) diff --git a/indexer/postgres/testing/postgres_test.go b/indexer/postgres/testing/postgres_test.go index d7af95cc1d8c..f9c6773f7e83 100644 --- a/indexer/postgres/testing/postgres_test.go +++ b/indexer/postgres/testing/postgres_test.go @@ -18,7 +18,7 @@ import ( "cosmossdk.io/schema/appdata" "cosmossdk.io/schema/indexing" indexertesting "cosmossdk.io/schema/testing" - appdatatest "cosmossdk.io/schema/testing/appdata" + "cosmossdk.io/schema/testing/appdatasim" "cosmossdk.io/schema/testing/statesim" ) @@ -65,7 +65,7 @@ func testPostgresIndexer(t *testing.T, retainDeletions bool) { res, err := indexer.Initialize(ctx, indexing.InitializationData{}) require.NoError(t, err) - fixture := appdatatest.NewSimulator(appdatatest.SimulatorOptions{ + fixture := appdatasim.NewSimulator(appdatasim.SimulatorOptions{ Listener: appdata.ListenerMux( appdata.DebugListener(os.Stdout), res.Listener, diff --git a/schema/testing/appdata/json.go b/schema/testing/appdata/json.go deleted file mode 100644 index 379bfee8faf3..000000000000 --- a/schema/testing/appdata/json.go +++ /dev/null @@ -1,40 +0,0 @@ -package appdatatest - -import ( - "pgregory.net/rapid" - - "cosmossdk.io/schema/appdata" -) - -func jsonValueGen() *rapid.Generator[any] { - return rapid.OneOf( - rapid.Bool().AsAny(), - rapid.Float64().AsAny(), - rapid.String().AsAny(), - rapid.MapOf(rapid.String(), rapid.Deferred(jsonValueGen)).AsAny(), - rapid.SliceOf(rapid.Deferred(jsonValueGen)).AsAny(), - ) -} - -var JSONValueGen = jsonValueGen() - -var JSONObjectGen = rapid.MapOf(rapid.String(), JSONValueGen) - -var JSONArrayGen = rapid.SliceOf(JSONValueGen) - -func JSONObjectWithKeys(keys ...string) *rapid.Generator[map[string]interface{}] { - return rapid.MapOf(rapid.SampledFrom(keys), JSONValueGen) -} - -func StringMapWithKeys(keys ...string) *rapid.Generator[map[string]string] { - return rapid.MapOf(rapid.SampledFrom(keys), rapid.String()) -} - -// events can consist of names separated by dots, e.g. "message.sent" -const eventTypeFormat = `^([a-zA-Z_][a-zA-Z0-9_]*\.)*[A-Za-z_][A-Za-z0-9_]$` - -var DefaultEventDataGen = rapid.Custom(func(t *rapid.T) appdata.EventData { - return appdata.EventData{ - Type: rapid.StringMatching(`^$`).Draw(t, "type"), - } -}) diff --git a/schema/testing/appdata/app_data.go b/schema/testing/appdatasim/app_data.go similarity index 99% rename from schema/testing/appdata/app_data.go rename to schema/testing/appdatasim/app_data.go index 1c53f78716c9..a99c12a55aad 100644 --- a/schema/testing/appdata/app_data.go +++ b/schema/testing/appdatasim/app_data.go @@ -1,4 +1,4 @@ -package appdatatest +package appdatasim import ( "fmt" diff --git a/schema/testing/appdata/app_data_test.go b/schema/testing/appdatasim/app_data_test.go similarity index 96% rename from schema/testing/appdata/app_data_test.go rename to schema/testing/appdatasim/app_data_test.go index 3a43e917a8e5..fc11c45f7297 100644 --- a/schema/testing/appdata/app_data_test.go +++ b/schema/testing/appdatasim/app_data_test.go @@ -1,4 +1,4 @@ -package appdatatest +package appdatasim import ( "bytes" diff --git a/schema/testing/appdata/testdata/app_sim_example_schema.txt b/schema/testing/appdatasim/testdata/app_sim_example_schema.txt similarity index 100% rename from schema/testing/appdata/testdata/app_sim_example_schema.txt rename to schema/testing/appdatasim/testdata/app_sim_example_schema.txt diff --git a/schema/testing/appdata/write_listener.go b/schema/testing/appdatasim/write_listener.go similarity index 97% rename from schema/testing/appdata/write_listener.go rename to schema/testing/appdatasim/write_listener.go index 41a1dcb24043..e0524b19a33d 100644 --- a/schema/testing/appdata/write_listener.go +++ b/schema/testing/appdatasim/write_listener.go @@ -1,4 +1,4 @@ -package appdatatest +package appdatasim import ( "encoding/json" From 102353eaa677c4fb41aa669e91fb564c414fdb0d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 19:32:48 +0200 Subject: [PATCH 39/79] cleanup --- schema/field.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/schema/field.go b/schema/field.go index c537fc68df8c..6e421df10cc3 100644 --- a/schema/field.go +++ b/schema/field.go @@ -22,10 +22,6 @@ type Field struct { // the same values for the same enum name. This possibly introduces some duplication of // definitions but makes it easier to reason about correctness and validation in isolation. EnumDefinition EnumDefinition - - // References is the name of another field in the same module schema that this field references. - // It must be in the form of ".", ex. "Account.address". - References string } // Validate validates the field. From 6343cd201c7c705d299db84354ec4f7512328d1a Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 19:37:19 +0200 Subject: [PATCH 40/79] cleanup --- schema/README.md | 2 +- schema/decoder.go | 34 +++++----------------------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/schema/README.md b/schema/README.md index 316a57e67386..d19d54c9e8c4 100644 --- a/schema/README.md +++ b/schema/README.md @@ -2,7 +2,7 @@ The `cosmossdk.io/schema` base module is designed to provide a stable, **zero-dependency** base layer for specifying the **logical representation of module state schemas** and implementing **state indexing**. This is intended to be used primarily for indexing modules in external databases and providing a standard human-readable state representation for genesis import and export. -The schema defined in this library does not aim to be general purpose and cover all types of schemas, such as those used for defining transactions. For instance, this schema is not include many types of composite objects as nested objects and arrays. Rather, the schema defined here aims to cover _state_ schemas only which are implemented as key-value pairs and usually have direct mappings to relational database tables or objects in a document store. +The schema defined in this library does not aim to be general purpose and cover all types of schemas, such as those used for defining transactions. For instance, this schema does not include many types of composite objects such as nested objects and arrays. Rather, the schema defined here aims to cover _state_ schemas only which are implemented as key-value pairs and usually have direct mappings to relational database tables or objects in a document store. Also, this schema does not cover physical state layout and byte-level encoding, but simply describes a common logical format. diff --git a/schema/decoder.go b/schema/decoder.go index 9165f1c04e40..1d228d1c06ae 100644 --- a/schema/decoder.go +++ b/schema/decoder.go @@ -1,7 +1,5 @@ package schema -import "context" - // HasModuleCodec is an interface that modules can implement to provide a ModuleCodec. // Usually these modules would also implement appmodule.AppModule, but that is not included // to keep this package free of any dependencies. @@ -18,32 +16,10 @@ type ModuleCodec struct { // KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. // If it is nil, the module doesn't support state decoding directly. KVDecoder KVDecoder - - // ApplyUpdate is a function that applies an ObjectUpdate to the module's state for the given context. - // If it is nil, the module doesn't support applying logical updates. If this function is provided - // then it can be used as a genesis import path. - ApplyUpdate ApplyUpdate -} - -// KVDecoder is a function that decodes a key-value pair into one or more ObjectUpdate's. -// If the KV-pair doesn't represent object updates, the function should return nil as the first -// and no error. The error result should only be non-nil when the decoder expected -// to parse a valid update and was unable to. In the case of an error, the decoder may return -// a non-nil value for the first return value, which can indicate which parts of the update -// were decodable to aid debugging. -type KVDecoder = func(KVPairUpdate) ([]ObjectUpdate, error) - -type KVPairUpdate struct { - // Key is the key of the key-value pair. - Key []byte - - // Value is the value of the key-value pair. It should be ignored when Delete is true. - Value []byte - - // Delete is a flag that indicates that the key-value pair was deleted. If it is false, - // then it is assumed that this has been a set operation. - Delete bool } -// ApplyUpdate is a function that applies an ObjectUpdate to the module's state for the given context. -type ApplyUpdate = func(context.Context, ObjectUpdate) error +// KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. +// If the KV-pair doesn't represent an object update, the function should return false +// as the second return value. Error should only be non-nil when the decoder expected +// to parse a valid update and was unable to. +type KVDecoder = func(key, value []byte) (ObjectUpdate, bool, error) From 055b2b06229f2745ebe1bb75a6918a4c5b26a4a2 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 19:41:10 +0200 Subject: [PATCH 41/79] refactoring --- indexer/postgres/indexer.go | 4 ++-- schema/indexing/indexer.go | 4 ++-- schema/{log => logutil}/logger.go | 14 +++++++++----- 3 files changed, 13 insertions(+), 9 deletions(-) rename schema/{log => logutil}/logger.go (59%) diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index 4d711e280257..ac9325f85931 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -8,7 +8,7 @@ import ( "cosmossdk.io/schema/appdata" "cosmossdk.io/schema/indexing" - "cosmossdk.io/schema/log" + "cosmossdk.io/schema/logutil" ) type Indexer struct { @@ -86,7 +86,7 @@ func init() { type Options struct { RetainDeletions bool - Logger log.Logger + Logger logutil.Logger } func NewIndexer(db *sql.DB, opts Options) (*Indexer, error) { diff --git a/schema/indexing/indexer.go b/schema/indexing/indexer.go index 926392952153..7568df1a3f8a 100644 --- a/schema/indexing/indexer.go +++ b/schema/indexing/indexer.go @@ -5,7 +5,7 @@ import ( "fmt" "cosmossdk.io/schema/appdata" - "cosmossdk.io/schema/log" + "cosmossdk.io/schema/logutil" ) type Indexer interface { @@ -13,7 +13,7 @@ type Indexer interface { } type IndexerResources struct { - Logger log.Logger + Logger logutil.Logger } type IndexerFactory = func(options map[string]interface{}, resources IndexerResources) (Indexer, error) diff --git a/schema/log/logger.go b/schema/logutil/logger.go similarity index 59% rename from schema/log/logger.go rename to schema/logutil/logger.go index 2007d6cc5cc0..cb6b34ebfd2b 100644 --- a/schema/log/logger.go +++ b/schema/logutil/logger.go @@ -1,5 +1,8 @@ -package log +// Package logutil defines the Logger interface expected by indexer implementations. +// It is implemented by cosmossdk.io/log which is not imported to minimize dependencies. +package logutil +// Logger is the logger interface expected by indexer implementations. type Logger interface { // Info takes a message and a set of key/value pairs and logs with level INFO. // The key of the tuple must be a string. @@ -18,14 +21,15 @@ type Logger interface { Debug(msg string, keyVals ...interface{}) } +// NoopLogger is a logger that doesn't do anything. type NoopLogger struct{} -func (n NoopLogger) Info(msg string, keyVals ...interface{}) {} +func (n NoopLogger) Info(string, ...interface{}) {} -func (n NoopLogger) Warn(msg string, keyVals ...interface{}) {} +func (n NoopLogger) Warn(string, ...interface{}) {} -func (n NoopLogger) Error(msg string, keyVals ...interface{}) {} +func (n NoopLogger) Error(string, ...interface{}) {} -func (n NoopLogger) Debug(msg string, keyVals ...interface{}) {} +func (n NoopLogger) Debug(string, ...interface{}) {} var _ Logger = NoopLogger{} From 19c74afc5da0bb0aafdc2be3786d80de1df83c19 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 22:58:27 +0200 Subject: [PATCH 42/79] updates --- indexer/postgres/base_sql.go | 7 +++++++ indexer/postgres/column.go | 11 +++++++---- indexer/postgres/create_table.go | 4 ++-- indexer/postgres/indexer.go | 5 +++++ schema/decoder.go | 24 +++++++++++++++++++----- 5 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 indexer/postgres/base_sql.go diff --git a/indexer/postgres/base_sql.go b/indexer/postgres/base_sql.go new file mode 100644 index 000000000000..7b82b05a20c5 --- /dev/null +++ b/indexer/postgres/base_sql.go @@ -0,0 +1,7 @@ +package postgres + +const baseSql = ` +CREATE OR REPLACE FUNCTION nanos_to_timestamptz(nanos bigint) RETURNS timestamptz AS $$ + SELECT to_timestamp(nanos / 1000000000) + nanos * INTERVAL '1 microsecond' +$$ LANGUAGE SQL IMMUTABLE; +` diff --git a/indexer/postgres/column.go b/indexer/postgres/column.go index 05ec015812de..77e9e79a2909 100644 --- a/indexer/postgres/column.go +++ b/indexer/postgres/column.go @@ -7,7 +7,8 @@ import ( "cosmossdk.io/schema" ) -func (tm *TableManager) createColumnDef(writer io.Writer, field schema.Field) error { +// createColumnDefinition writes a column definition within a CREATE TABLE statement for the field. +func (tm *TableManager) createColumnDefinition(writer io.Writer, field schema.Field) error { _, err := fmt.Fprintf(writer, "%q ", field.Name) if err != nil { return err @@ -37,7 +38,7 @@ func (tm *TableManager) createColumnDef(writer io.Writer, field schema.Field) er case schema.TimeKind: nanosCol := fmt.Sprintf("%s_nanos", field.Name) // TODO: retain at least microseconds in the timestamp - _, err = fmt.Fprintf(writer, "TIMESTAMPTZ GENERATED ALWAYS AS (to_timestamp(%q / 1000000000)) STORED,\n\t", nanosCol) + _, err = fmt.Fprintf(writer, "TIMESTAMPTZ GENERATED ALWAYS AS (nanos_to_timestamptz(%q)) STORED,\n\t", nanosCol) if err != nil { return err } @@ -99,13 +100,15 @@ func simpleColumnType(kind schema.Kind) string { case schema.JSONKind: return "JSONB" case schema.DurationKind: - // TODO: set COMMENT on field indicating nanoseconds unit - return "BIGINT" // TODO: maybe convert to postgres interval + return "BIGINT" default: return "" } } +// updatableColumnName is the name of the insertable/updatable column name for the field. +// This is the field name in most cases, except for time columns which are stored as nanos +// and then converted to timestamp generated columns. func (tm *TableManager) updatableColumnName(field schema.Field) (name string, err error) { name = field.Name if field.Kind == schema.TimeKind { diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index 477548d84347..b879c7588e46 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -33,7 +33,7 @@ func (tm *TableManager) CreateTableSql(writer io.Writer) error { _, err = fmt.Fprintf(writer, "_id INTEGER NOT NULL CHECK (_id = 1),\n\t") } else { for _, field := range tm.typ.KeyFields { - err = tm.createColumnDef(writer, field) + err = tm.createColumnDefinition(writer, field) if err != nil { return err } @@ -41,7 +41,7 @@ func (tm *TableManager) CreateTableSql(writer io.Writer) error { } for _, field := range tm.typ.ValueFields { - err = tm.createColumnDef(writer, field) + err = tm.createColumnDefinition(writer, field) if err != nil { return err } diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index ac9325f85931..10a385acd6d0 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -38,6 +38,11 @@ func (i *Indexer) Initialize(ctx context.Context, data indexing.InitializationDa return indexing.InitializationResult{}, fmt.Errorf("failed to start transaction: %w", err) } + _, err = tx.ExecContext(ctx, baseSql) + if err != nil { + return indexing.InitializationResult{}, err + } + i.tx = tx return indexing.InitializationResult{ diff --git a/schema/decoder.go b/schema/decoder.go index 1d228d1c06ae..161ac619566f 100644 --- a/schema/decoder.go +++ b/schema/decoder.go @@ -18,8 +18,22 @@ type ModuleCodec struct { KVDecoder KVDecoder } -// KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. -// If the KV-pair doesn't represent an object update, the function should return false -// as the second return value. Error should only be non-nil when the decoder expected -// to parse a valid update and was unable to. -type KVDecoder = func(key, value []byte) (ObjectUpdate, bool, error) +// KVDecoder is a function that decodes a key-value pair into one or more ObjectUpdate's. +// If the KV-pair doesn't represent object updates, the function should return nil as the first +// and no error. The error result should only be non-nil when the decoder expected +// to parse a valid update and was unable to. In the case of an error, the decoder may return +// a non-nil value for the first return value, which can indicate which parts of the update +// were decodable to aid debugging. +type KVDecoder = func(KVPairUpdate) ([]ObjectUpdate, error) + +type KVPairUpdate struct { + // Key is the key of the key-value pair. + Key []byte + + // Value is the value of the key-value pair. It should be ignored when Delete is true. + Value []byte + + // Delete is a flag that indicates that the key-value pair was deleted. If it is false, + // then it is assumed that this has been a set operation. + Delete bool +} From 1c49db58a05e0f6ad819037b6b00f3de66bf7c7b Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 23:16:23 +0200 Subject: [PATCH 43/79] docs, cleanup --- indexer/postgres/base_sql.go | 3 ++- indexer/postgres/conn.go | 14 ++++++++++++ indexer/postgres/count.go | 6 ++--- indexer/postgres/create_table.go | 6 ++--- indexer/postgres/delete.go | 9 +++++--- indexer/postgres/enum.go | 22 ++++++++++-------- indexer/postgres/indexer.go | 10 +++----- indexer/postgres/insert_update.go | 10 ++++---- indexer/postgres/module_mgr.go | 28 +++++++++++++---------- indexer/postgres/params.go | 1 + indexer/postgres/select.go | 18 ++++++++++----- indexer/postgres/table_mgr.go | 1 + indexer/postgres/testing/postgres_test.go | 6 ++--- indexer/postgres/where.go | 2 ++ 14 files changed, 85 insertions(+), 51 deletions(-) create mode 100644 indexer/postgres/conn.go diff --git a/indexer/postgres/base_sql.go b/indexer/postgres/base_sql.go index 7b82b05a20c5..1e4467e5f819 100644 --- a/indexer/postgres/base_sql.go +++ b/indexer/postgres/base_sql.go @@ -1,6 +1,7 @@ package postgres -const baseSql = ` +// BaseSQL is the base SQL that is always included in the schema. +const BaseSQL = ` CREATE OR REPLACE FUNCTION nanos_to_timestamptz(nanos bigint) RETURNS timestamptz AS $$ SELECT to_timestamp(nanos / 1000000000) + nanos * INTERVAL '1 microsecond' $$ LANGUAGE SQL IMMUTABLE; diff --git a/indexer/postgres/conn.go b/indexer/postgres/conn.go new file mode 100644 index 000000000000..de8c1cac6b47 --- /dev/null +++ b/indexer/postgres/conn.go @@ -0,0 +1,14 @@ +package postgres + +import ( + "context" + "database/sql" +) + +// DBConn is an interface that abstracts the *sql.DB, *sql.Tx and *sql.Conn types. +type DBConn interface { + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) + QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) + QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row +} diff --git a/indexer/postgres/count.go b/indexer/postgres/count.go index d5b4c8eb5b08..16f74d627d02 100644 --- a/indexer/postgres/count.go +++ b/indexer/postgres/count.go @@ -2,12 +2,12 @@ package postgres import ( "context" - "database/sql" "fmt" ) -func (tm *TableManager) Count(ctx context.Context, tx *sql.Tx) (int, error) { - row := tx.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %q;", tm.TableName())) +// Count returns the number of rows in the table. +func (tm *TableManager) Count(ctx context.Context, conn DBConn) (int, error) { + row := conn.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %q;", tm.TableName())) var count int err := row.Scan(&count) return count, err diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index b879c7588e46..e7b23e6842fb 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -2,13 +2,13 @@ package postgres import ( "context" - "database/sql" "fmt" "io" "strings" ) -func (tm *TableManager) CreateTable(ctx context.Context, tx *sql.Tx) error { +// CreateTable creates the table for the object type. +func (tm *TableManager) CreateTable(ctx context.Context, conn DBConn) error { buf := new(strings.Builder) err := tm.CreateTableSql(buf) if err != nil { @@ -17,7 +17,7 @@ func (tm *TableManager) CreateTable(ctx context.Context, tx *sql.Tx) error { sqlStr := buf.String() tm.options.Logger.Debug("Creating table", "table", tm.TableName(), "sql", sqlStr) - _, err = tx.ExecContext(ctx, sqlStr) + _, err = conn.ExecContext(ctx, sqlStr) return err } diff --git a/indexer/postgres/delete.go b/indexer/postgres/delete.go index d6ad911d42bb..317e5db19af8 100644 --- a/indexer/postgres/delete.go +++ b/indexer/postgres/delete.go @@ -2,13 +2,13 @@ package postgres import ( "context" - "database/sql" "fmt" "io" "strings" ) -func (tm *TableManager) Delete(ctx context.Context, tx *sql.Tx, key interface{}) error { +// Delete deletes the row with the provided key from the table. +func (tm *TableManager) Delete(ctx context.Context, conn DBConn, key interface{}) error { buf := new(strings.Builder) var params []interface{} var err error @@ -23,10 +23,11 @@ func (tm *TableManager) Delete(ctx context.Context, tx *sql.Tx, key interface{}) sqlStr := buf.String() tm.options.Logger.Debug("Delete", "sql", sqlStr, "params", params) - _, err = tx.ExecContext(ctx, sqlStr, params...) + _, err = conn.ExecContext(ctx, sqlStr, params...) return err } +// DeleteSqlAndParams generates a DELETE statement and binding parameters for the provided key. func (tm *TableManager) DeleteSqlAndParams(w io.Writer, key interface{}) ([]interface{}, error) { _, err := fmt.Fprintf(w, "DELETE FROM %q", tm.TableName()) if err != nil { @@ -42,6 +43,8 @@ func (tm *TableManager) DeleteSqlAndParams(w io.Writer, key interface{}) ([]inte return keyParams, err } +// RetainDeleteSqlAndParams generates an UPDATE statement to set the _deleted column to true for the provided key +// which is used when the table is set to retain deletions mode. func (tm *TableManager) RetainDeleteSqlAndParams(w io.Writer, key interface{}) ([]interface{}, error) { _, err := fmt.Fprintf(w, "UPDATE %q SET _deleted = TRUE", tm.TableName()) if err != nil { diff --git a/indexer/postgres/enum.go b/indexer/postgres/enum.go index 49cbf8b740b5..4295a4bd819d 100644 --- a/indexer/postgres/enum.go +++ b/indexer/postgres/enum.go @@ -10,7 +10,8 @@ import ( "cosmossdk.io/schema" ) -func (m *ModuleManager) createEnumTypesForFields(ctx context.Context, tx *sql.Tx, fields []schema.Field) error { +// createEnumTypesForFields creates enum types for all the fields that have enum kind in the module schema. +func (m *ModuleManager) createEnumTypesForFields(ctx context.Context, conn DBConn, fields []schema.Field) error { for _, field := range fields { if field.Kind != schema.EnumKind { continue @@ -22,7 +23,7 @@ func (m *ModuleManager) createEnumTypesForFields(ctx context.Context, tx *sql.Tx continue } - err := m.CreateEnumType(ctx, tx, field.EnumDefinition) + err := m.CreateEnumType(ctx, conn, field.EnumDefinition) if err != nil { return err } @@ -33,13 +34,10 @@ func (m *ModuleManager) createEnumTypesForFields(ctx context.Context, tx *sql.Tx return nil } -func enumTypeName(moduleName string, enum schema.EnumDefinition) string { - return fmt.Sprintf("%s_%s", moduleName, enum.Name) -} - -func (m *ModuleManager) CreateEnumType(ctx context.Context, tx *sql.Tx, enum schema.EnumDefinition) error { +// CreateEnumType creates an enum type in the database. +func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum schema.EnumDefinition) error { typeName := enumTypeName(m.moduleName, enum) - row := tx.QueryRowContext(ctx, "SELECT 1 FROM pg_type WHERE typname = $1", typeName) + row := conn.QueryRowContext(ctx, "SELECT 1 FROM pg_type WHERE typname = $1", typeName) var res interface{} if err := row.Scan(&res); err != nil { if err != sql.ErrNoRows { @@ -58,10 +56,11 @@ func (m *ModuleManager) CreateEnumType(ctx context.Context, tx *sql.Tx, enum sch sqlStr := buf.String() m.options.Logger.Debug("Creating enum type", "sql", sqlStr) - _, err = tx.ExecContext(ctx, sqlStr) + _, err = conn.ExecContext(ctx, sqlStr) return err } +// CreateEnumTypeSql generates a CREATE TYPE statement for the enum definition. func (m *ModuleManager) CreateEnumTypeSql(writer io.Writer, enum schema.EnumDefinition) error { _, err := fmt.Fprintf(writer, "CREATE TYPE %q AS ENUM (", enumTypeName(m.moduleName, enum)) if err != nil { @@ -84,3 +83,8 @@ func (m *ModuleManager) CreateEnumTypeSql(writer io.Writer, enum schema.EnumDefi _, err = fmt.Fprintf(writer, ");") return err } + +// enumTypeName returns the name of the enum type scoped to the module. +func enumTypeName(moduleName string, enum schema.EnumDefinition) string { + return fmt.Sprintf("%s_%s", moduleName, enum.Name) +} diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index 10a385acd6d0..c55b12c23908 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -38,7 +38,7 @@ func (i *Indexer) Initialize(ctx context.Context, data indexing.InitializationDa return indexing.InitializationResult{}, fmt.Errorf("failed to start transaction: %w", err) } - _, err = tx.ExecContext(ctx, baseSql) + _, err = tx.ExecContext(ctx, BaseSQL) if err != nil { return indexing.InitializationResult{}, err } @@ -121,7 +121,7 @@ func (i *Indexer) initModuleSchema(data appdata.ModuleInitializationData) error mm := newModuleManager(moduleName, modSchema, i.options) i.modules[moduleName] = mm - return mm.Init(i.ctx, i.tx) + return mm.InitializeSchema(i.ctx, i.tx) } func (i *Indexer) onObjectUpdate(data appdata.ObjectUpdateData) error { @@ -132,7 +132,7 @@ func (i *Indexer) onObjectUpdate(data appdata.ObjectUpdateData) error { } for _, update := range data.Updates { - tm, ok := mod.Tables[update.TypeName] + tm, ok := mod.tables[update.TypeName] if !ok { return fmt.Errorf("object type %s not found in schema for module %s", update.TypeName, module) } @@ -160,10 +160,6 @@ func (i *Indexer) commit(_ appdata.CommitData) error { return err } -func (i *Indexer) ActiveTx() *sql.Tx { - return i.tx -} - func (i *Indexer) Modules() map[string]*ModuleManager { return i.modules } diff --git a/indexer/postgres/insert_update.go b/indexer/postgres/insert_update.go index af651da7ad5a..cc68cb219cc9 100644 --- a/indexer/postgres/insert_update.go +++ b/indexer/postgres/insert_update.go @@ -2,14 +2,14 @@ package postgres import ( "context" - "database/sql" "fmt" "io" "strings" ) -func (tm *TableManager) InsertUpdate(ctx context.Context, tx *sql.Tx, key, value interface{}) error { - exists, err := tm.Exists(ctx, tx, key) +// InsertUpdate inserts or updates the row with the provided key and value. +func (tm *TableManager) InsertUpdate(ctx context.Context, conn DBConn, key, value interface{}) error { + exists, err := tm.Exists(ctx, conn, key) if err != nil { return err } @@ -24,10 +24,11 @@ func (tm *TableManager) InsertUpdate(ctx context.Context, tx *sql.Tx, key, value sqlStr := buf.String() tm.options.Logger.Debug("Insert or Update", "sql", sqlStr, "params", params) - _, err = tx.ExecContext(ctx, sqlStr, params...) + _, err = conn.ExecContext(ctx, sqlStr, params...) return err } +// InsertSql generates an INSERT statement and binding parameters for the provided key and value. func (tm *TableManager) InsertSql(w io.Writer, key, value interface{}) ([]interface{}, error) { keyParams, keyCols, err := tm.bindKeyParams(key) if err != nil { @@ -59,6 +60,7 @@ func (tm *TableManager) InsertSql(w io.Writer, key, value interface{}) ([]interf return allParams, err } +// UpdateSql generates an UPDATE statement and binding parameters for the provided key and value. func (tm *TableManager) UpdateSql(w io.Writer, key, value interface{}) ([]interface{}, error) { _, err := fmt.Fprintf(w, "UPDATE %q SET ", tm.TableName()) diff --git a/indexer/postgres/module_mgr.go b/indexer/postgres/module_mgr.go index 8d6c7f403db8..fa907bf5b16e 100644 --- a/indexer/postgres/module_mgr.go +++ b/indexer/postgres/module_mgr.go @@ -2,17 +2,16 @@ package postgres import ( "context" - "database/sql" "fmt" "cosmossdk.io/schema" ) +// ModuleManager manages the tables for a module. type ModuleManager struct { - moduleName string - schema schema.ModuleSchema - // TODO: make private or internal - Tables map[string]*TableManager + moduleName string + schema schema.ModuleSchema + tables map[string]*TableManager definedEnums map[string]schema.EnumDefinition options Options } @@ -21,32 +20,32 @@ func newModuleManager(moduleName string, modSchema schema.ModuleSchema, options return &ModuleManager{ moduleName: moduleName, schema: modSchema, - Tables: map[string]*TableManager{}, + tables: map[string]*TableManager{}, definedEnums: map[string]schema.EnumDefinition{}, options: options, } } -func (m *ModuleManager) Init(ctx context.Context, tx *sql.Tx) error { +// InitializeSchema creates tables for all object types in the module schema and creates enum types. +func (m *ModuleManager) InitializeSchema(ctx context.Context, conn DBConn) error { // create enum types for _, typ := range m.schema.ObjectTypes { - err := m.createEnumTypesForFields(ctx, tx, typ.KeyFields) + err := m.createEnumTypesForFields(ctx, conn, typ.KeyFields) if err != nil { return err } - err = m.createEnumTypesForFields(ctx, tx, typ.ValueFields) + err = m.createEnumTypesForFields(ctx, conn, typ.ValueFields) if err != nil { return err } } // create tables for all object types - // NOTE: if we want to support foreign keys, we need to sort tables ind dependency order for _, typ := range m.schema.ObjectTypes { tm := NewTableManager(m.moduleName, typ, m.options) - m.Tables[typ.Name] = tm - err := tm.CreateTable(ctx, tx) + m.tables[typ.Name] = tm + err := tm.CreateTable(ctx, conn) if err != nil { return fmt.Errorf("failed to create table for %s in module %s: %w", typ.Name, m.moduleName, err) } @@ -55,3 +54,8 @@ func (m *ModuleManager) Init(ctx context.Context, tx *sql.Tx) error { return nil } + +// Tables returns the table managers for the module. +func (m *ModuleManager) Tables() map[string]*TableManager { + return m.tables +} diff --git a/indexer/postgres/params.go b/indexer/postgres/params.go index 1d9d04072161..3ef4352b6b95 100644 --- a/indexer/postgres/params.go +++ b/indexer/postgres/params.go @@ -9,6 +9,7 @@ import ( "cosmossdk.io/schema" ) +// bindKeyParams binds the key to the key columns. func (tm *TableManager) bindKeyParams(key interface{}) ([]interface{}, []string, error) { n := len(tm.typ.KeyFields) if n == 0 { diff --git a/indexer/postgres/select.go b/indexer/postgres/select.go index 5325353f9942..830a628566b3 100644 --- a/indexer/postgres/select.go +++ b/indexer/postgres/select.go @@ -8,16 +8,18 @@ import ( "strings" ) -func (tm *TableManager) Exists(ctx context.Context, tx *sql.Tx, key interface{}) (bool, error) { +// Exists checks if a row with the provided key exists in the table. +func (tm *TableManager) Exists(ctx context.Context, conn DBConn, key interface{}) (bool, error) { buf := new(strings.Builder) params, err := tm.ExistsSqlAndParams(buf, key) if err != nil { return false, err } - return tm.checkExists(ctx, tx, buf.String(), params) + return tm.checkExists(ctx, conn, buf.String(), params) } +// ExistsSqlAndParams generates a SELECT statement to check if a row with the provided key exists in the table. func (tm *TableManager) ExistsSqlAndParams(w io.Writer, key interface{}) ([]interface{}, error) { _, err := fmt.Fprintf(w, "SELECT 1 FROM %q", tm.TableName()) if err != nil { @@ -33,16 +35,18 @@ func (tm *TableManager) ExistsSqlAndParams(w io.Writer, key interface{}) ([]inte return keyParams, err } -func (tm *TableManager) Equals(ctx context.Context, tx *sql.Tx, key, val interface{}) (bool, error) { +// Equals checks if a row with the provided key and value exists. +func (tm *TableManager) Equals(ctx context.Context, conn DBConn, key, val interface{}) (bool, error) { buf := new(strings.Builder) params, err := tm.EqualsSqlAndParams(buf, key, val) if err != nil { return false, err } - return tm.checkExists(ctx, tx, buf.String(), params) + return tm.checkExists(ctx, conn, buf.String(), params) } +// EqualsSqlAndParams generates a SELECT statement to check if a row with the provided key and value exists in the table. func (tm *TableManager) EqualsSqlAndParams(w io.Writer, key, val interface{}) ([]interface{}, error) { _, err := fmt.Fprintf(w, "SELECT 1 FROM %q", tm.TableName()) if err != nil { @@ -76,10 +80,12 @@ func (tm *TableManager) EqualsSqlAndParams(w io.Writer, key, val interface{}) ([ return allParams, err } -func (tm *TableManager) checkExists(ctx context.Context, tx *sql.Tx, sqlStr string, params []interface{}) (bool, error) { +// checkExists checks if a row exists in the table. +func (tm *TableManager) checkExists(ctx context.Context, conn DBConn, sqlStr string, params []interface{}) (bool, error) { tm.options.Logger.Debug("Select", "sql", sqlStr, "params", params) var res interface{} - err := tx.QueryRowContext(ctx, sqlStr, params...).Scan(&res) + // TODO check for multiple rows which would be a logic error + err := conn.QueryRowContext(ctx, sqlStr, params...).Scan(&res) switch err { case nil: return true, nil diff --git a/indexer/postgres/table_mgr.go b/indexer/postgres/table_mgr.go index 9210230bcc41..f08eaccbf625 100644 --- a/indexer/postgres/table_mgr.go +++ b/indexer/postgres/table_mgr.go @@ -38,6 +38,7 @@ func NewTableManager(moduleName string, typ schema.ObjectType, options Options) } } +// TableName returns the name of the table for the object type scoped to its module. func (tm *TableManager) TableName() string { return fmt.Sprintf("%s_%s", tm.moduleName, tm.typ.Name) } diff --git a/indexer/postgres/testing/postgres_test.go b/indexer/postgres/testing/postgres_test.go index f9c6773f7e83..d6bec39eaaa4 100644 --- a/indexer/postgres/testing/postgres_test.go +++ b/indexer/postgres/testing/postgres_test.go @@ -86,18 +86,18 @@ func testPostgresIndexer(t *testing.T, retainDeletions bool) { require.NoError(t, fixture.AppState().ScanObjectCollections(func(moduleName string, collection *statesim.ObjectCollection) error { modMgr, ok := indexer.Modules()[moduleName] require.True(t, ok) - tblMgr, ok := modMgr.Tables[collection.ObjectType().Name] + tblMgr, ok := modMgr.Tables()[collection.ObjectType().Name] require.True(t, ok) expectedCount := collection.Len() - actualCount, err := tblMgr.Count(context.Background(), indexer.ActiveTx()) + actualCount, err := tblMgr.Count(context.Background(), db) require.NoError(t, err) require.Equalf(t, expectedCount, actualCount, "table %s %s count mismatch", moduleName, collection.ObjectType().Name) return collection.ScanState(func(update schema.ObjectUpdate) error { found, err := tblMgr.Equals( context.Background(), - indexer.ActiveTx(), update.Key, update.Value) + db, update.Key, update.Value) if err != nil { return err } diff --git a/indexer/postgres/where.go b/indexer/postgres/where.go index 81e63fc20de6..3f10838829a1 100644 --- a/indexer/postgres/where.go +++ b/indexer/postgres/where.go @@ -5,6 +5,7 @@ import ( "io" ) +// WhereSqlAndParams generates a WHERE clause for the provided key and returns the parameters. func (tm *TableManager) WhereSqlAndParams(w io.Writer, key interface{}, startParamIdx int) (endParamIdx int, keyParams []interface{}, err error) { var keyCols []string keyParams, keyCols, err = tm.bindKeyParams(key) @@ -16,6 +17,7 @@ func (tm *TableManager) WhereSqlAndParams(w io.Writer, key interface{}, startPar return } +// WhereSql generates a WHERE clause for the provided columns and returns the parameters. func (tm *TableManager) WhereSql(w io.Writer, params []interface{}, cols []string, startParamIdx int) (endParamIdx int, resParams []interface{}, err error) { _, err = fmt.Fprintf(w, " WHERE ") if err != nil { From 396a65bd49ad6635631e1ded77ba6736723eee5d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 23:18:56 +0200 Subject: [PATCH 44/79] time fix --- indexer/postgres/base_sql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indexer/postgres/base_sql.go b/indexer/postgres/base_sql.go index 1e4467e5f819..81e1ac704242 100644 --- a/indexer/postgres/base_sql.go +++ b/indexer/postgres/base_sql.go @@ -3,6 +3,6 @@ package postgres // BaseSQL is the base SQL that is always included in the schema. const BaseSQL = ` CREATE OR REPLACE FUNCTION nanos_to_timestamptz(nanos bigint) RETURNS timestamptz AS $$ - SELECT to_timestamp(nanos / 1000000000) + nanos * INTERVAL '1 microsecond' + SELECT to_timestamp(nanos / 1000000000) + (nanos / 1000000000) * INTERVAL '1 microsecond' $$ LANGUAGE SQL IMMUTABLE; ` From 447c01346605cb1a2a8af865f65f83ad65da3ca5 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 23:25:49 +0200 Subject: [PATCH 45/79] docs, fixes --- indexer/postgres/column.go | 2 ++ indexer/postgres/create_table.go | 34 +++++++++++--------------------- indexer/postgres/indexer.go | 8 +++++--- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/indexer/postgres/column.go b/indexer/postgres/column.go index 77e9e79a2909..f49cf7a212b6 100644 --- a/indexer/postgres/column.go +++ b/indexer/postgres/column.go @@ -55,6 +55,7 @@ func (tm *TableManager) createColumnDefinition(writer io.Writer, field schema.Fi } } +// writeNullability writes column nullability. func writeNullability(writer io.Writer, nullable bool) error { if nullable { _, err := fmt.Fprintf(writer, " NULL,\n\t") @@ -65,6 +66,7 @@ func writeNullability(writer io.Writer, nullable bool) error { } } +// simpleColumnType returns the postgres column type for the kind for simple types. func simpleColumnType(kind schema.Kind) string { switch kind { case schema.StringKind: diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index e7b23e6842fb..e6331d222209 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -31,6 +31,9 @@ func (tm *TableManager) CreateTableSql(writer io.Writer) error { if len(tm.typ.KeyFields) == 0 { isSingleton = true _, err = fmt.Fprintf(writer, "_id INTEGER NOT NULL CHECK (_id = 1),\n\t") + if err != nil { + return err + } } else { for _, field := range tm.typ.KeyFields { err = tm.createColumnDefinition(writer, field) @@ -48,7 +51,6 @@ func (tm *TableManager) CreateTableSql(writer io.Writer) error { } // add _deleted column when we have RetainDeletions set and enabled - // NOTE: needs more design if tm.options.RetainDeletions && tm.typ.RetainDeletions { _, err = fmt.Fprintf(writer, "_deleted BOOLEAN NOT NULL DEFAULT FALSE,\n\t") if err != nil { @@ -75,29 +77,15 @@ func (tm *TableManager) CreateTableSql(writer io.Writer) error { return err } - // TODO: we need test data to not generate constraint failures to safely enable this - //for _, uniq := range tm.typ.UniqueConstraints { - // cols := make([]string, len(uniq.FieldNames)) - // for i, name := range uniq.FieldNames { - // field, ok := tm.allFields[name] - // if !ok { - // return fmt.Errorf("unknown field %q in unique constraint", name) - // } - // - // cols[i], err = tm.updatableColumnName(field) - // if err != nil { - // return err - // } - // } - // - // _, err = fmt.Fprintf(writer, ",\n\tUNIQUE NULLS NOT DISTINCT (%s)", strings.Join(cols, ", ")) - //} - - _, err = fmt.Fprintf(writer, ` -); + _, err = fmt.Fprintf(writer, "\n);\n") + if err != nil { + return err + } -GRANT SELECT ON TABLE %q TO PUBLIC; -`, tm.TableName()) + // we GRANT SELECT on the table to PUBLIC so that the table is automatically available + // for querying using off-the-shelf tools like pg_graphql, Postgrest, Postgraphile, etc. + // without any login permissions + _, err = fmt.Fprintf(writer, "GRANT SELECT ON TABLE %q TO PUBLIC;", tm.TableName()) if err != nil { return err } diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index c55b12c23908..fc67c1209beb 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -51,9 +51,10 @@ func (i *Indexer) Initialize(ctx context.Context, data indexing.InitializationDa } type configOptions struct { - DatabaseDriver string `json:"database_driver"` - DatabaseURL string `json:"database_url"` - RetainDeletions bool `json:"retain_deletions"` + DatabaseDriver string `json:"database_driver"` + DatabaseURL string `json:"database_url"` + // TODO should probably default to true + RetainDeletions bool `json:"retain_deletions"` } func init() { @@ -160,6 +161,7 @@ func (i *Indexer) commit(_ appdata.CommitData) error { return err } +// Modules retains the module managers for the indexer. func (i *Indexer) Modules() map[string]*ModuleManager { return i.modules } From 04e925e9cfc434aa978ac71a53ac9d6621663880 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 2 Jul 2024 23:34:55 +0200 Subject: [PATCH 46/79] docs, cleanup --- schema/testing/statesim/app.go | 34 +++++++++++++++++-------------- schema/testing/statesim/module.go | 7 +++++++ schema/testing/statesim/object.go | 12 ++++++++++- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/schema/testing/statesim/app.go b/schema/testing/statesim/app.go index b242429e73f0..0a1a3b8f4893 100644 --- a/schema/testing/statesim/app.go +++ b/schema/testing/statesim/app.go @@ -11,11 +11,13 @@ import ( "cosmossdk.io/schema/appdata" ) +// App is a collection of simulated module states corresponding to an app's schema for testing purposes. type App struct { moduleStates *btree.Map[string, *Module] updateGen *rapid.Generator[appdata.ObjectUpdateData] } +// NewApp creates a new simulation App for the given app schema. func NewApp(appSchema map[string]schema.ModuleSchema, options Options) *App { moduleStates := &btree.Map[string, *Module]{} var moduleNames []string @@ -53,19 +55,30 @@ func NewApp(appSchema map[string]schema.ModuleSchema, options Options) *App { } } -func (a *App) ApplyUpdate(moduleName string, update schema.ObjectUpdate) error { - moduleState, ok := a.moduleStates.Get(moduleName) +// ApplyUpdate applies the given object update to the module. +func (a *App) ApplyUpdate(data appdata.ObjectUpdateData) error { + moduleState, ok := a.moduleStates.Get(data.ModuleName) if !ok { - return fmt.Errorf("module %s not found", moduleName) + return fmt.Errorf("module %s not found", data.ModuleName) } - return moduleState.ApplyUpdate(update) + for _, update := range data.Updates { + err := moduleState.ApplyUpdate(update) + if err != nil { + return err + } + } + + return nil } +// UpdateGen is a generator for object update data against the app. It is stateful and includes a certain number of +// updates and deletions to existing objects. func (a *App) UpdateGen() *rapid.Generator[appdata.ObjectUpdateData] { return a.updateGen } +// ScanModuleSchemas iterates over all the modules schemas in the app. func (a *App) ScanModuleSchemas(f func(string, schema.ModuleSchema) error) error { var err error a.moduleStates.Scan(func(key string, value *Module) bool { @@ -75,21 +88,12 @@ func (a *App) ScanModuleSchemas(f func(string, schema.ModuleSchema) error) error return err } +// GetModule returns the module state for the given module name. func (a *App) GetModule(moduleName string) (*Module, bool) { return a.moduleStates.Get(moduleName) } -func (a *App) ScanState(f func(moduleName string, update schema.ObjectUpdate) error) error { - var err error - a.moduleStates.Scan(func(moduleName string, value *Module) bool { - err = value.ScanState(func(update schema.ObjectUpdate) error { - return f(moduleName, update) - }) - return err == nil - }) - return err -} - +// ScanObjectCollections iterates over all the object collections in the app. func (a *App) ScanObjectCollections(f func(moduleName string, collection *ObjectCollection) error) error { var err error a.moduleStates.Scan(func(moduleName string, value *Module) bool { diff --git a/schema/testing/statesim/module.go b/schema/testing/statesim/module.go index 15c2f663ee69..ded9c82bac85 100644 --- a/schema/testing/statesim/module.go +++ b/schema/testing/statesim/module.go @@ -10,12 +10,14 @@ import ( "cosmossdk.io/schema" ) +// Module is a collection of object collections corresponding to a module's schema for testing purposes. type Module struct { moduleSchema schema.ModuleSchema objectCollections *btree.Map[string, *ObjectCollection] updateGen *rapid.Generator[schema.ObjectUpdate] } +// NewModule creates a new Module for the given module schema. func NewModule(moduleSchema schema.ModuleSchema, options Options) *Module { objectCollections := &btree.Map[string, *ObjectCollection]{} var objectTypeNames []string @@ -43,6 +45,7 @@ func NewModule(moduleSchema schema.ModuleSchema, options Options) *Module { } } +// ApplyUpdate applies the given object update to the module. func (o *Module) ApplyUpdate(update schema.ObjectUpdate) error { objState, ok := o.objectCollections.Get(update.TypeName) if !ok { @@ -52,14 +55,18 @@ func (o *Module) ApplyUpdate(update schema.ObjectUpdate) error { return objState.ApplyUpdate(update) } +// UpdateGen returns a generator for object updates. The generator is stateful and returns +// a certain number of updates and deletes of existing objects in the module. func (o *Module) UpdateGen() *rapid.Generator[schema.ObjectUpdate] { return o.updateGen } +// GetObjectCollection returns the object collection for the given object type. func (o *Module) GetObjectCollection(objectType string) (*ObjectCollection, bool) { return o.objectCollections.Get(objectType) } +// ScanState scans the state of all object collections in the module. func (o *Module) ScanState(f func(schema.ObjectUpdate) error) error { var err error o.objectCollections.Scan(func(key string, value *ObjectCollection) bool { diff --git a/schema/testing/statesim/object.go b/schema/testing/statesim/object.go index 6b11361ea7ca..3c6e95593de4 100644 --- a/schema/testing/statesim/object.go +++ b/schema/testing/statesim/object.go @@ -10,6 +10,7 @@ import ( schematesting "cosmossdk.io/schema/testing" ) +// ObjectCollection is a collection of objects of a specific type for testing purposes. type ObjectCollection struct { options Options objectType schema.ObjectType @@ -18,6 +19,7 @@ type ObjectCollection struct { valueFieldIndices map[string]int } +// NewObjectCollection creates a new ObjectCollection for the given object type. func NewObjectCollection(objectType schema.ObjectType, options Options) *ObjectCollection { objects := &btree.Map[string, schema.ObjectUpdate]{} updateGen := schematesting.ObjectUpdateGen(objectType, objects) @@ -35,6 +37,7 @@ func NewObjectCollection(objectType schema.ObjectType, options Options) *ObjectC } } +// ApplyUpdate applies the given object update to the collection. func (o *ObjectCollection) ApplyUpdate(update schema.ObjectUpdate) error { if update.TypeName != o.objectType.Name { return fmt.Errorf("update type name %q does not match object type name %q", update.TypeName, o.objectType.Name) @@ -91,10 +94,13 @@ func (o *ObjectCollection) ApplyUpdate(update schema.ObjectUpdate) error { return nil } +// UpdateGen returns a generator for random object updates against the collection. This generator +// is stateful and returns a certain number of updates and deletes to existing objects. func (o *ObjectCollection) UpdateGen() *rapid.Generator[schema.ObjectUpdate] { return o.updateGen } +// ScanState scans the state of the collection by calling the given function for each object update. func (o *ObjectCollection) ScanState(f func(schema.ObjectUpdate) error) error { var err error o.objects.Scan(func(_ string, v schema.ObjectUpdate) bool { @@ -104,14 +110,18 @@ func (o *ObjectCollection) ScanState(f func(schema.ObjectUpdate) error) error { return err } -func (o *ObjectCollection) GetObject(key any) (schema.ObjectUpdate, bool) { +// GetObject returns the object with the given key from the collection represented as an ObjectUpdate +// itself. Deletions that are retained are returned as ObjectUpdate's with delete set to true. +func (o *ObjectCollection) GetObject(key any) (update schema.ObjectUpdate, found bool) { return o.objects.Get(fmt.Sprintf("%v", key)) } +// ObjectType returns the object type of the collection. func (o *ObjectCollection) ObjectType() schema.ObjectType { return o.objectType } +// Len returns the number of objects in the collection. func (o *ObjectCollection) Len() int { return o.objects.Len() } From 005e2ffed27c0a55d9e22fdd720d2dc01793d5df Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 12:52:17 +0200 Subject: [PATCH 47/79] refactoring and docs --- indexer/postgres/column.go | 18 +++--- indexer/postgres/testing/postgres_test.go | 47 +++++++------- schema/testing/README.md | 3 +- schema/testing/app.go | 1 + schema/testing/appdatasim/app_data.go | 70 ++++++++++++++------- schema/testing/appdatasim/app_data_test.go | 2 +- schema/testing/appdatasim/doc.go | 2 + schema/testing/appdatasim/write_listener.go | 2 + schema/testing/doc.go | 3 + schema/testing/enum.go | 1 + schema/testing/field.go | 9 +++ schema/testing/module_schema.go | 5 +- schema/testing/name.go | 1 + schema/testing/object.go | 16 +++-- schema/testing/statesim/app.go | 20 +----- schema/testing/statesim/doc.go | 3 + schema/testing/statesim/module.go | 13 ++-- 17 files changed, 128 insertions(+), 88 deletions(-) create mode 100644 schema/testing/appdatasim/doc.go create mode 100644 schema/testing/doc.go create mode 100644 schema/testing/statesim/doc.go diff --git a/indexer/postgres/column.go b/indexer/postgres/column.go index f49cf7a212b6..aa980d385561 100644 --- a/indexer/postgres/column.go +++ b/indexer/postgres/column.go @@ -29,21 +29,17 @@ func (tm *TableManager) createColumnDefinition(writer io.Writer, field schema.Fi if err != nil { return err } - case schema.Bech32AddressKind: - _, err = fmt.Fprintf(writer, "TEXT") - if err != nil { - return err - } - case schema.TimeKind: - nanosCol := fmt.Sprintf("%s_nanos", field.Name) - // TODO: retain at least microseconds in the timestamp - _, err = fmt.Fprintf(writer, "TIMESTAMPTZ GENERATED ALWAYS AS (nanos_to_timestamptz(%q)) STORED,\n\t", nanosCol) + // for time fields, we generate two columns: + // - one with nanoseconds precision for lossless storage, suffixed with _nanos + // - one as a timestamptz (microsecond precision) for ease of use, that is GENERATED + nanosColName := fmt.Sprintf("%s_nanos", field.Name) + _, err = fmt.Fprintf(writer, "TIMESTAMPTZ GENERATED ALWAYS AS (nanos_to_timestamptz(%q)) STORED,\n\t", nanosColName) if err != nil { return err } - _, err = fmt.Fprintf(writer, `%q BIGINT`, nanosCol) + _, err = fmt.Fprintf(writer, `%q BIGINT`, nanosColName) if err != nil { return err } @@ -103,6 +99,8 @@ func simpleColumnType(kind schema.Kind) string { return "JSONB" case schema.DurationKind: return "BIGINT" + case schema.Bech32AddressKind: + return "TEXT" default: return "" } diff --git a/indexer/postgres/testing/postgres_test.go b/indexer/postgres/testing/postgres_test.go index d6bec39eaaa4..7d59d76804b3 100644 --- a/indexer/postgres/testing/postgres_test.go +++ b/indexer/postgres/testing/postgres_test.go @@ -65,7 +65,7 @@ func testPostgresIndexer(t *testing.T, retainDeletions bool) { res, err := indexer.Initialize(ctx, indexing.InitializationData{}) require.NoError(t, err) - fixture := appdatasim.NewSimulator(appdatasim.SimulatorOptions{ + fixture := appdatasim.NewSimulator(appdatasim.Options{ Listener: appdata.ListenerMux( appdata.DebugListener(os.Stdout), res.Listener, @@ -83,30 +83,33 @@ func testPostgresIndexer(t *testing.T, retainDeletions bool) { blockData := blockDataGen.Example(i) require.NoError(t, fixture.ProcessBlockData(blockData)) - require.NoError(t, fixture.AppState().ScanObjectCollections(func(moduleName string, collection *statesim.ObjectCollection) error { + require.NoError(t, fixture.AppState().ScanModules(func(moduleName string, mod *statesim.Module) error { modMgr, ok := indexer.Modules()[moduleName] require.True(t, ok) - tblMgr, ok := modMgr.Tables()[collection.ObjectType().Name] - require.True(t, ok) - - expectedCount := collection.Len() - actualCount, err := tblMgr.Count(context.Background(), db) - require.NoError(t, err) - require.Equalf(t, expectedCount, actualCount, "table %s %s count mismatch", moduleName, collection.ObjectType().Name) - - return collection.ScanState(func(update schema.ObjectUpdate) error { - found, err := tblMgr.Equals( - context.Background(), - db, update.Key, update.Value) - if err != nil { - return err - } - - if !found { - return fmt.Errorf("object not found in table %s %s %v %v", moduleName, collection.ObjectType().Name, update.Key, update.Value) - } - return nil + return mod.ScanObjectCollections(func(collection *statesim.ObjectCollection) error { + tblMgr, ok := modMgr.Tables()[collection.ObjectType().Name] + require.True(t, ok) + + expectedCount := collection.Len() + actualCount, err := tblMgr.Count(context.Background(), db) + require.NoError(t, err) + require.Equalf(t, expectedCount, actualCount, "table %s %s count mismatch", moduleName, collection.ObjectType().Name) + + return collection.ScanState(func(update schema.ObjectUpdate) error { + found, err := tblMgr.Equals( + context.Background(), + db, update.Key, update.Value) + if err != nil { + return err + } + + if !found { + return fmt.Errorf("object not found in table %s %s %v %v", moduleName, collection.ObjectType().Name, update.Key, update.Value) + } + + return nil + }) }) })) } diff --git a/schema/testing/README.md b/schema/testing/README.md index 5b89dcc2c685..36f73a566c83 100644 --- a/schema/testing/README.md +++ b/schema/testing/README.md @@ -1,4 +1,3 @@ # Schema Testing -This module contains core test utilities and fixtures for testing schema and indexer functionality. It is managed as a separate go module to manage versions better and allow for dependencies on useful testing libraries without imposing those -elsewhere. \ No newline at end of file +This module contains core test utilities and fixtures for testing schema and indexer functionality. It is managed as a separate go module to manage versions better and allow for dependencies on useful testing libraries without imposing those elsewhere. \ No newline at end of file diff --git a/schema/testing/app.go b/schema/testing/app.go index 56f50e03eab4..feda6d036346 100644 --- a/schema/testing/app.go +++ b/schema/testing/app.go @@ -8,6 +8,7 @@ import ( "cosmossdk.io/schema" ) +// AppSchemaGen generates random valid app schemas, essentially a map of module names to module schemas. var AppSchemaGen = rapid.Custom(func(t *rapid.T) map[string]schema.ModuleSchema { schema := make(map[string]schema.ModuleSchema) numModules := rapid.IntRange(1, 10).Draw(t, "numModules") diff --git a/schema/testing/appdatasim/app_data.go b/schema/testing/appdatasim/app_data.go index a99c12a55aad..cf80226bfcb0 100644 --- a/schema/testing/appdatasim/app_data.go +++ b/schema/testing/appdatasim/app_data.go @@ -11,7 +11,8 @@ import ( "cosmossdk.io/schema/testing/statesim" ) -type SimulatorOptions struct { +// Options are the options for creating an app data Simulator. +type Options struct { AppSchema map[string]schema.ModuleSchema Listener appdata.Listener EventAlignedWrites bool @@ -21,16 +22,19 @@ type SimulatorOptions struct { EventDataGen *rapid.Generator[appdata.EventData] } +// Simulator simulates a stream of app data. type Simulator struct { state *statesim.App - options SimulatorOptions + options Options blockNum uint64 blockDataGen *rapid.Generator[BlockData] } +// BlockData represents the app data packets in a block. type BlockData = []appdata.Packet -func NewSimulator(options SimulatorOptions) *Simulator { +// NewSimulator creates a new app data simulator with the given options. +func NewSimulator(options Options) *Simulator { if options.AppSchema == nil { options.AppSchema = schematesting.ExampleAppSchema } @@ -43,10 +47,11 @@ func NewSimulator(options SimulatorOptions) *Simulator { return sim } +// Initialize runs the initialization methods of the app data stream. func (a *Simulator) Initialize() error { if f := a.options.Listener.InitializeModuleData; f != nil { - err := a.state.ScanModuleSchemas(func(moduleName string, moduleSchema schema.ModuleSchema) error { - return f(appdata.ModuleInitializationData{ModuleName: moduleName, Schema: moduleSchema}) + err := a.state.ScanModules(func(moduleName string, mod *statesim.Module) error { + return f(appdata.ModuleInitializationData{ModuleName: moduleName, Schema: mod.ModuleSchema()}) }) if err != nil { return err @@ -56,16 +61,22 @@ func (a *Simulator) Initialize() error { return nil } +// BlockDataGen generates random block data. It is expected that generated data is passed to ProcessBlockData +// to simulate the app data stream and advance app state based on the object updates in the block. The first +// packet in the block data will be a StartBlockData packet with the height set to the next block height. func (a *Simulator) BlockDataGen() *rapid.Generator[BlockData] { return a.BlockDataGenN(100) } +// BlockDataGenN creates a block data generator which allows specifying the maximum number of updates per block. func (a *Simulator) BlockDataGenN(maxUpdatesPerBlock int) *rapid.Generator[BlockData] { numUpdatesGen := rapid.IntRange(1, maxUpdatesPerBlock) return rapid.Custom(func(t *rapid.T) BlockData { var packets BlockData + packets = append(packets, appdata.StartBlockData{Height: a.blockNum + 1}) + updateSet := map[string]bool{} // filter out any updates to the same key from this block, otherwise we can end up with weird errors updateGen := a.state.UpdateGen().Filter(func(data appdata.ObjectUpdateData) bool { @@ -86,38 +97,43 @@ func (a *Simulator) BlockDataGenN(maxUpdatesPerBlock int) *rapid.Generator[Block packets = append(packets, data) } + packets = append(packets, appdata.CommitData{}) + return packets }) } +// ProcessBlockData processes the given block data, advancing the app state based on the object updates in the block +// and forwarding all packets to the attached listener. It is expected that the data passed came from BlockDataGen, +// however, other data can be passed as long as the first packet is a StartBlockData packet with the block height +// set to the current block height + 1 and the last packet is a CommitData packet. func (a *Simulator) ProcessBlockData(data BlockData) error { - a.blockNum++ + if len(data) < 2 { + return fmt.Errorf("block data must contain at least two packets") + } - if f := a.options.Listener.StartBlock; f != nil { - err := f(appdata.StartBlockData{Height: a.blockNum}) - if err != nil { - return err - } + if startBlock, ok := data[0].(appdata.StartBlockData); !ok || startBlock.Height != a.blockNum+1 { + return fmt.Errorf("first packet in block data must be a StartBlockData packet with height %d", a.blockNum+1) } - for _, packet := range data { - err := a.options.Listener.SendPacket(packet) - if err != nil { - return err - } + if _, ok := data[len(data)-1].(appdata.CommitData); !ok { + return fmt.Errorf("last packet in block data must be a CommitData packet") + } + // advance the block height + a.blockNum++ + + for _, packet := range data { + // apply state updates from object updates if updateData, ok := packet.(appdata.ObjectUpdateData); ok { - for _, update := range updateData.Updates { - err = a.state.ApplyUpdate(updateData.ModuleName, update) - if err != nil { - return err - } + err := a.state.ApplyUpdate(updateData) + if err != nil { + return err } } - } - if f := a.options.Listener.Commit; f != nil { - err := f(appdata.CommitData{}) + // send the packet to the listener + err := a.options.Listener.SendPacket(packet) if err != nil { return err } @@ -126,6 +142,12 @@ func (a *Simulator) ProcessBlockData(data BlockData) error { return nil } +// AppState returns the current app state backing the simulator. func (a *Simulator) AppState() *statesim.App { return a.state } + +// BlockNum returns the current block number of the simulator. +func (a *Simulator) BlockNum() uint64 { + return a.blockNum +} diff --git a/schema/testing/appdatasim/app_data_test.go b/schema/testing/appdatasim/app_data_test.go index fc11c45f7297..61bd212efccc 100644 --- a/schema/testing/appdatasim/app_data_test.go +++ b/schema/testing/appdatasim/app_data_test.go @@ -12,7 +12,7 @@ import ( func TestAppSimulator_ExampleSchema(t *testing.T) { out := &bytes.Buffer{} - appSim := NewSimulator(SimulatorOptions{ + appSim := NewSimulator(Options{ AppSchema: schematesting.ExampleAppSchema, Listener: WriterListener(out), }) diff --git a/schema/testing/appdatasim/doc.go b/schema/testing/appdatasim/doc.go new file mode 100644 index 000000000000..7f3493e2524b --- /dev/null +++ b/schema/testing/appdatasim/doc.go @@ -0,0 +1,2 @@ +// Package appdatasim contains utilities for simulating valid streams of app data for testing indexer implementations. +package appdatasim diff --git a/schema/testing/appdatasim/write_listener.go b/schema/testing/appdatasim/write_listener.go index e0524b19a33d..a176612bd9a8 100644 --- a/schema/testing/appdatasim/write_listener.go +++ b/schema/testing/appdatasim/write_listener.go @@ -8,6 +8,8 @@ import ( "cosmossdk.io/schema/appdata" ) +// WriterListener returns an appdata.Listener that writes events to the given io.Writer deterministically +// for testing purposes. func WriterListener(w io.Writer) appdata.Listener { return appdata.Listener{ StartBlock: func(data appdata.StartBlockData) error { diff --git a/schema/testing/doc.go b/schema/testing/doc.go new file mode 100644 index 000000000000..05f3dcb1b496 --- /dev/null +++ b/schema/testing/doc.go @@ -0,0 +1,3 @@ +// Package schematesting includes property-based testing generators for creating random valid data +// for testing schemas and state representing those schemas. +package schematesting diff --git a/schema/testing/enum.go b/schema/testing/enum.go index f4aee77c9dba..cd244929c0b0 100644 --- a/schema/testing/enum.go +++ b/schema/testing/enum.go @@ -8,6 +8,7 @@ import ( var enumValuesGen = rapid.SliceOfNDistinct(NameGen, 1, 10, func(x string) string { return x }) +// EnumDefinitionGen generates random valid EnumDefinitions. var EnumDefinitionGen = rapid.Custom(func(t *rapid.T) schema.EnumDefinition { enum := schema.EnumDefinition{ Name: NameGen.Draw(t, "name"), diff --git a/schema/testing/field.go b/schema/testing/field.go index 8803e4b3150f..5ea8c202407c 100644 --- a/schema/testing/field.go +++ b/schema/testing/field.go @@ -17,6 +17,7 @@ var ( boolGen = rapid.Bool() ) +// FieldGen generates random Field's based on the validity criteria of fields. var FieldGen = rapid.Custom(func(t *rapid.T) schema.Field { kind := kindGen.Draw(t, "kind") field := schema.Field{ @@ -36,6 +37,8 @@ var FieldGen = rapid.Custom(func(t *rapid.T) schema.Field { return field }) +// FieldValueGen generates random valid values for the field, aiming to exercise the full range of possible +// values for the field. func FieldValueGen(field schema.Field) *rapid.Generator[any] { gen := baseFieldValue(field) @@ -97,6 +100,7 @@ func baseFieldValue(field schema.Field) *rapid.Generator[any] { } } +// KeyFieldsValueGen generates a value that is valid for the provided key fields. func KeyFieldsValueGen(keyFields []schema.Field) *rapid.Generator[any] { if len(keyFields) == 0 { return rapid.Just[any](nil) @@ -120,6 +124,11 @@ func KeyFieldsValueGen(keyFields []schema.Field) *rapid.Generator[any] { }) } +// ValueFieldsValueGen generates a value that is valid for the provided value fields. The +// forUpdate parameter indicates whether the generator should generate value that +// are valid for insertion (in the case forUpdate is false) or for update (in the case forUpdate is true). +// Values that are for update may skip some fields in a ValueUpdates instance whereas values for insertion +// will always contain all values. func ValueFieldsValueGen(valueFields []schema.Field, forUpdate bool) *rapid.Generator[any] { // special case where there are no value fields // we shouldn't end up here, but just in case diff --git a/schema/testing/module_schema.go b/schema/testing/module_schema.go index ee36232acf2f..276b189522c4 100644 --- a/schema/testing/module_schema.go +++ b/schema/testing/module_schema.go @@ -8,6 +8,7 @@ import ( "cosmossdk.io/schema" ) +// ModuleSchemaGen generates random ModuleSchema's based on the validity criteria of module schemas. var ModuleSchemaGen = rapid.Custom(func(t *rapid.T) schema.ModuleSchema { schema := schema.ModuleSchema{} numObjectTypes := rapid.IntRange(1, 10).Draw(t, "numObjectTypes") @@ -20,10 +21,10 @@ var ModuleSchemaGen = rapid.Custom(func(t *rapid.T) schema.ModuleSchema { // filter out enums with duplicate names enumTypeNames := map[string]bool{} for _, objectType := range schema.ObjectTypes { - if !checkDuplicateEnumName(enumTypeNames, objectType.KeyFields) { + if !hasDuplicateEnumName(enumTypeNames, objectType.KeyFields) { return false } - if !checkDuplicateEnumName(enumTypeNames, objectType.ValueFields) { + if !hasDuplicateEnumName(enumTypeNames, objectType.ValueFields) { return false } } diff --git a/schema/testing/name.go b/schema/testing/name.go index 89d21faccec8..144347e5db46 100644 --- a/schema/testing/name.go +++ b/schema/testing/name.go @@ -6,4 +6,5 @@ import ( "cosmossdk.io/schema" ) +// NameGen validates valid names that match the NameFormat regex. var NameGen = rapid.StringMatching(schema.NameFormat) diff --git a/schema/testing/object.go b/schema/testing/object.go index 09e77052d0dd..f834d9fb1320 100644 --- a/schema/testing/object.go +++ b/schema/testing/object.go @@ -11,6 +11,7 @@ var fieldsGen = rapid.SliceOfNDistinct(FieldGen, 1, 12, func(f schema.Field) str return f.Name }) +// ObjectTypeGen generates random ObjectType's based on the validity criteria of object types. var ObjectTypeGen = rapid.Custom(func(t *rapid.T) schema.ObjectType { typ := schema.ObjectType{ Name: NameGen.Draw(t, "name"), @@ -34,34 +35,39 @@ var ObjectTypeGen = rapid.Custom(func(t *rapid.T) schema.ObjectType { }).Filter(func(typ schema.ObjectType) bool { // filter out duplicate enum names enumTypeNames := map[string]bool{} - if !checkDuplicateEnumName(enumTypeNames, typ.KeyFields) { + if hasDuplicateEnumName(enumTypeNames, typ.KeyFields) { return false } - if !checkDuplicateEnumName(enumTypeNames, typ.ValueFields) { + if hasDuplicateEnumName(enumTypeNames, typ.ValueFields) { return false } return true }) -func checkDuplicateEnumName(enumTypeNames map[string]bool, fields []schema.Field) bool { +// hasDuplicateEnumName checks if there is an enum field with a duplicate name +// in the object type +func hasDuplicateEnumName(enumTypeNames map[string]bool, fields []schema.Field) bool { for _, field := range fields { if field.Kind != schema.EnumKind { continue } if _, ok := enumTypeNames[field.EnumDefinition.Name]; ok { - return false + return true } enumTypeNames[field.EnumDefinition.Name] = true } - return true + return false } +// ObjectInsertGen generates object updates that are valid for insertion. func ObjectInsertGen(objectType schema.ObjectType) *rapid.Generator[schema.ObjectUpdate] { return ObjectUpdateGen(objectType, nil) } +// ObjectUpdateGen generates object updates that are valid for updates using the provided state map as a source +// of valid existing keys. func ObjectUpdateGen(objectType schema.ObjectType, state *btree.Map[string, schema.ObjectUpdate]) *rapid.Generator[schema.ObjectUpdate] { keyGen := KeyFieldsValueGen(objectType.KeyFields) diff --git a/schema/testing/statesim/app.go b/schema/testing/statesim/app.go index 0a1a3b8f4893..1319e5573e48 100644 --- a/schema/testing/statesim/app.go +++ b/schema/testing/statesim/app.go @@ -78,29 +78,15 @@ func (a *App) UpdateGen() *rapid.Generator[appdata.ObjectUpdateData] { return a.updateGen } -// ScanModuleSchemas iterates over all the modules schemas in the app. -func (a *App) ScanModuleSchemas(f func(string, schema.ModuleSchema) error) error { - var err error - a.moduleStates.Scan(func(key string, value *Module) bool { - err = f(key, value.moduleSchema) - return err == nil - }) - return err -} - // GetModule returns the module state for the given module name. func (a *App) GetModule(moduleName string) (*Module, bool) { return a.moduleStates.Get(moduleName) } -// ScanObjectCollections iterates over all the object collections in the app. -func (a *App) ScanObjectCollections(f func(moduleName string, collection *ObjectCollection) error) error { +func (a *App) ScanModules(f func(moduleName string, modState *Module) error) error { var err error - a.moduleStates.Scan(func(moduleName string, value *Module) bool { - value.objectCollections.Scan(func(key string, value *ObjectCollection) bool { - err = f(moduleName, value) - return err == nil - }) + a.moduleStates.Scan(func(key string, value *Module) bool { + err = f(key, value) return err == nil }) return err diff --git a/schema/testing/statesim/doc.go b/schema/testing/statesim/doc.go new file mode 100644 index 000000000000..9f06ff8eb71c --- /dev/null +++ b/schema/testing/statesim/doc.go @@ -0,0 +1,3 @@ +// Package statesim contains utilities for simulating state based on ObjectType's and ModuleSchema's for testing +// the conformance of state management libraries and indexers to schema rules. +package statesim diff --git a/schema/testing/statesim/module.go b/schema/testing/statesim/module.go index ded9c82bac85..699cb2924922 100644 --- a/schema/testing/statesim/module.go +++ b/schema/testing/statesim/module.go @@ -61,18 +61,21 @@ func (o *Module) UpdateGen() *rapid.Generator[schema.ObjectUpdate] { return o.updateGen } +// ModuleSchema returns the module schema for the module. +func (o *Module) ModuleSchema() schema.ModuleSchema { + return o.moduleSchema +} + // GetObjectCollection returns the object collection for the given object type. func (o *Module) GetObjectCollection(objectType string) (*ObjectCollection, bool) { return o.objectCollections.Get(objectType) } -// ScanState scans the state of all object collections in the module. -func (o *Module) ScanState(f func(schema.ObjectUpdate) error) error { +// ScanObjectCollections scans all object collections in the module. +func (o *Module) ScanObjectCollections(f func(value *ObjectCollection) error) error { var err error o.objectCollections.Scan(func(key string, value *ObjectCollection) bool { - err = value.ScanState(func(update schema.ObjectUpdate) error { - return f(update) - }) + err = f(value) return err == nil }) return err From 06bfeab1def740f1920ec8bfeb57e6d20dcd2921 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 12:58:55 +0200 Subject: [PATCH 48/79] feat(schema)!: updates based on postgres testing --- schema/decoder.go | 24 +++++++-- schema/enum.go | 4 ++ schema/field.go | 3 +- schema/kind.go | 31 +++++++----- schema/kind_test.go | 100 ++++++++++++++++++------------------- schema/object_type.go | 6 ++- schema/object_type_test.go | 14 ++++++ 7 files changed, 113 insertions(+), 69 deletions(-) diff --git a/schema/decoder.go b/schema/decoder.go index 1d228d1c06ae..161ac619566f 100644 --- a/schema/decoder.go +++ b/schema/decoder.go @@ -18,8 +18,22 @@ type ModuleCodec struct { KVDecoder KVDecoder } -// KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. -// If the KV-pair doesn't represent an object update, the function should return false -// as the second return value. Error should only be non-nil when the decoder expected -// to parse a valid update and was unable to. -type KVDecoder = func(key, value []byte) (ObjectUpdate, bool, error) +// KVDecoder is a function that decodes a key-value pair into one or more ObjectUpdate's. +// If the KV-pair doesn't represent object updates, the function should return nil as the first +// and no error. The error result should only be non-nil when the decoder expected +// to parse a valid update and was unable to. In the case of an error, the decoder may return +// a non-nil value for the first return value, which can indicate which parts of the update +// were decodable to aid debugging. +type KVDecoder = func(KVPairUpdate) ([]ObjectUpdate, error) + +type KVPairUpdate struct { + // Key is the key of the key-value pair. + Key []byte + + // Value is the value of the key-value pair. It should be ignored when Delete is true. + Value []byte + + // Delete is a flag that indicates that the key-value pair was deleted. If it is false, + // then it is assumed that this has been a set operation. + Delete bool +} diff --git a/schema/enum.go b/schema/enum.go index 0ed710af37f9..d70458d63889 100644 --- a/schema/enum.go +++ b/schema/enum.go @@ -5,6 +5,10 @@ import "fmt" // EnumDefinition represents the definition of an enum type. type EnumDefinition struct { // Name is the name of the enum type. It must conform to the NameFormat regular expression. + // Its name must be unique between all enum types and object types in the module. + // The same enum, however, can be used in multiple object types and fields as long as the + // definition is identical each time + // TODO: uniqueness validation Name string // Values is a list of distinct, non-empty values that are part of the enum type. diff --git a/schema/field.go b/schema/field.go index a3b6ea2784cf..6e421df10cc3 100644 --- a/schema/field.go +++ b/schema/field.go @@ -10,10 +10,11 @@ type Field struct { // Kind is the basic type of the field. Kind Kind - // Nullable indicates whether null values are accepted for the field. + // Nullable indicates whether null values are accepted for the field. Key fields CANNOT be nullable. Nullable bool // AddressPrefix is the address prefix of the field's kind, currently only used for Bech32AddressKind. + // TODO: add validation for valid address prefixes AddressPrefix string // EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind. diff --git a/schema/kind.go b/schema/kind.go index 91602b0bd2bf..9a27eed5cab0 100644 --- a/schema/kind.go +++ b/schema/kind.go @@ -5,6 +5,7 @@ import ( "fmt" "regexp" "time" + "unicode/utf8" ) // Kind represents the basic type of a field in an object. @@ -16,7 +17,9 @@ const ( // InvalidKind indicates that an invalid type. InvalidKind Kind = iota - // StringKind is a string type and values of this type must be of the go type string. + // StringKind is a string type and values of this type must be of the go type string + // containing valid UTF-8 and cannot contain null characters. + // TODO: add validation for null characters StringKind // BytesKind is a bytes type and values of this type must be of the go type []byte. @@ -46,14 +49,14 @@ const ( // Uint64Kind is a uint64 type and values of this type must be of the go type uint64. Uint64Kind - // IntegerKind represents an arbitrary precision integer number. Values of this type must + // IntegerStringKind represents an arbitrary precision integer number. Values of this type must // be of the go type string and formatted as base10 integers, specifically matching to // the IntegerFormat regex. - IntegerKind + IntegerStringKind - // DecimalKind represents an arbitrary precision decimal or integer number. Values of this type + // DecimalStringKind represents an arbitrary precision decimal or integer number. Values of this type // must be of the go type string and match the DecimalFormat regex. - DecimalKind + DecimalStringKind // BoolKind is a boolean type and values of this type must be of the go type bool. BoolKind @@ -134,9 +137,9 @@ func (t Kind) String() string { return "int64" case Uint64Kind: return "uint64" - case DecimalKind: + case DecimalStringKind: return "decimal" - case IntegerKind: + case IntegerStringKind: return "integer" case BoolKind: return "bool" @@ -216,13 +219,13 @@ func (t Kind) ValidateValueType(value interface{}) error { if !ok { return fmt.Errorf("expected uint64, got %T", value) } - case IntegerKind: + case IntegerStringKind: _, ok := value.(string) if !ok { return fmt.Errorf("expected string, got %T", value) } - case DecimalKind: + case DecimalStringKind: _, ok := value.(string) if !ok { return fmt.Errorf("expected string, got %T", value) @@ -283,11 +286,15 @@ func (t Kind) ValidateValue(value interface{}) error { } switch t { - case IntegerKind: + case StringKind: + if !utf8.ValidString(value.(string)) { + return fmt.Errorf("expected valid utf-8 string, got %s", value) + } + case IntegerStringKind: if !integerRegex.Match([]byte(value.(string))) { return fmt.Errorf("expected base10 integer, got %s", value) } - case DecimalKind: + case DecimalStringKind: if !decimalRegex.Match([]byte(value.(string))) { return fmt.Errorf("expected decimal number, got %s", value) } @@ -307,7 +314,7 @@ var ( ) // KindForGoValue finds the simplest kind that can represent the given go value. It will not, however, -// return kinds such as IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind which all can be +// return kinds such as IntegerStringKind, DecimalStringKind, Bech32AddressKind, or EnumKind which all can be // represented as strings. func KindForGoValue(value interface{}) Kind { switch value.(type) { diff --git a/schema/kind_test.go b/schema/kind_test.go index c287bb61aa78..9027fbb70010 100644 --- a/schema/kind_test.go +++ b/schema/kind_test.go @@ -53,12 +53,12 @@ func TestKind_ValidateValueType(t *testing.T) { {kind: Int64Kind, value: int32(1), valid: false}, {kind: Uint64Kind, value: uint64(1), valid: true}, {kind: Uint64Kind, value: uint32(1), valid: false}, - {kind: IntegerKind, value: "1", valid: true}, - {kind: IntegerKind, value: int32(1), valid: false}, - {kind: DecimalKind, value: "1.0", valid: true}, - {kind: DecimalKind, value: "1", valid: true}, - {kind: DecimalKind, value: "1.1e4", valid: true}, - {kind: DecimalKind, value: int32(1), valid: false}, + {kind: IntegerStringKind, value: "1", valid: true}, + {kind: IntegerStringKind, value: int32(1), valid: false}, + {kind: DecimalStringKind, value: "1.0", valid: true}, + {kind: DecimalStringKind, value: "1", valid: true}, + {kind: DecimalStringKind, value: "1.1e4", valid: true}, + {kind: DecimalStringKind, value: int32(1), valid: false}, {kind: Bech32AddressKind, value: []byte("hello"), valid: true}, {kind: Bech32AddressKind, value: 1, valid: false}, {kind: BoolKind, value: true, valid: true}, @@ -111,54 +111,54 @@ func TestKind_ValidateValue(t *testing.T) { {Int32Kind, "abc", false}, {BytesKind, nil, false}, // check integer, decimal and json more thoroughly - {IntegerKind, "1", true}, - {IntegerKind, "0", true}, - {IntegerKind, "10", true}, - {IntegerKind, "-100", true}, - {IntegerKind, "1.0", false}, - {IntegerKind, "00", true}, // leading zeros are allowed - {IntegerKind, "001", true}, - {IntegerKind, "-01", true}, + {IntegerStringKind, "1", true}, + {IntegerStringKind, "0", true}, + {IntegerStringKind, "10", true}, + {IntegerStringKind, "-100", true}, + {IntegerStringKind, "1.0", false}, + {IntegerStringKind, "00", true}, // leading zeros are allowed + {IntegerStringKind, "001", true}, + {IntegerStringKind, "-01", true}, // 100 digits - {IntegerKind, "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", true}, + {IntegerStringKind, "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", true}, // more than 100 digits - {IntegerKind, "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", false}, - {IntegerKind, "", false}, - {IntegerKind, "abc", false}, - {IntegerKind, "abc100", false}, - {DecimalKind, "1.0", true}, - {DecimalKind, "0.0", true}, - {DecimalKind, "-100.075", true}, - {DecimalKind, "1002346.000", true}, - {DecimalKind, "0", true}, - {DecimalKind, "10", true}, - {DecimalKind, "-100", true}, - {DecimalKind, "1", true}, - {DecimalKind, "1.0e4", true}, - {DecimalKind, "1.0e-4", true}, - {DecimalKind, "1.0e+4", true}, - {DecimalKind, "1.0e", false}, - {DecimalKind, "1.0e4.0", false}, - {DecimalKind, "1.0e-4.0", false}, - {DecimalKind, "1.0e+4.0", false}, - {DecimalKind, "-1.0e-4", true}, - {DecimalKind, "-1.0e+4", true}, - {DecimalKind, "-1.0E4", true}, - {DecimalKind, "1E-9", true}, - {DecimalKind, "1E-99", true}, - {DecimalKind, "1E+9", true}, - {DecimalKind, "1E+99", true}, + {IntegerStringKind, "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", false}, + {IntegerStringKind, "", false}, + {IntegerStringKind, "abc", false}, + {IntegerStringKind, "abc100", false}, + {DecimalStringKind, "1.0", true}, + {DecimalStringKind, "0.0", true}, + {DecimalStringKind, "-100.075", true}, + {DecimalStringKind, "1002346.000", true}, + {DecimalStringKind, "0", true}, + {DecimalStringKind, "10", true}, + {DecimalStringKind, "-100", true}, + {DecimalStringKind, "1", true}, + {DecimalStringKind, "1.0e4", true}, + {DecimalStringKind, "1.0e-4", true}, + {DecimalStringKind, "1.0e+4", true}, + {DecimalStringKind, "1.0e", false}, + {DecimalStringKind, "1.0e4.0", false}, + {DecimalStringKind, "1.0e-4.0", false}, + {DecimalStringKind, "1.0e+4.0", false}, + {DecimalStringKind, "-1.0e-4", true}, + {DecimalStringKind, "-1.0e+4", true}, + {DecimalStringKind, "-1.0E4", true}, + {DecimalStringKind, "1E-9", true}, + {DecimalStringKind, "1E-99", true}, + {DecimalStringKind, "1E+9", true}, + {DecimalStringKind, "1E+99", true}, // 50 digits before and after the decimal point - {DecimalKind, "10000000000000000000000000000000000000000000000000.10000000000000000000000000000000000000000000000001", true}, + {DecimalStringKind, "10000000000000000000000000000000000000000000000000.10000000000000000000000000000000000000000000000001", true}, // too many digits before the decimal point - {DecimalKind, "10000000000000000000000000000000000000000000000000000000000000000000000000", false}, + {DecimalStringKind, "10000000000000000000000000000000000000000000000000000000000000000000000000", false}, // too many digits after the decimal point - {DecimalKind, "1.0000000000000000000000000000000000000000000000000000000000000000000000001", false}, + {DecimalStringKind, "1.0000000000000000000000000000000000000000000000000000000000000000000000001", false}, // exponent too big - {DecimalKind, "1E-999", false}, - {DecimalKind, "", false}, - {DecimalKind, "abc", false}, - {DecimalKind, "abc", false}, + {DecimalStringKind, "1E-999", false}, + {DecimalStringKind, "", false}, + {DecimalStringKind, "abc", false}, + {DecimalStringKind, "abc", false}, {JSONKind, json.RawMessage(`{"a":10}`), true}, {JSONKind, json.RawMessage("10"), true}, {JSONKind, json.RawMessage("10.0"), true}, @@ -200,8 +200,8 @@ func TestKind_String(t *testing.T) { {Uint32Kind, "uint32"}, {Int64Kind, "int64"}, {Uint64Kind, "uint64"}, - {IntegerKind, "integer"}, - {DecimalKind, "decimal"}, + {IntegerStringKind, "integer"}, + {DecimalStringKind, "decimal"}, {BoolKind, "bool"}, {TimeKind, "time"}, {DurationKind, "duration"}, diff --git a/schema/object_type.go b/schema/object_type.go index f991329eff0a..89306a07ccb9 100644 --- a/schema/object_type.go +++ b/schema/object_type.go @@ -11,7 +11,7 @@ type ObjectType struct { // KeyFields is a list of fields that make up the primary key of the object. // It can be empty in which case indexers should assume that this object is // a singleton and only has one value. Field names must be unique within the - // object between both key and value fields. + // object between both key and value fields. Key fields CANNOT be nullable. KeyFields []Field // ValueFields is a list of fields that are not part of the primary key of the object. @@ -39,6 +39,10 @@ func (o ObjectType) Validate() error { return fmt.Errorf("invalid key field %q: %w", field.Name, err) } + if field.Nullable { + return fmt.Errorf("key field %q cannot be nullable", field.Name) + } + if fieldNames[field.Name] { return fmt.Errorf("duplicate field name %q", field.Name) } diff --git a/schema/object_type_test.go b/schema/object_type_test.go index 32e8885bbce2..e945239f2c67 100644 --- a/schema/object_type_test.go +++ b/schema/object_type_test.go @@ -148,6 +148,20 @@ func TestObjectType_Validate(t *testing.T) { }, errContains: "duplicate field name", }, + { + name: "nullable key field", + objectType: ObjectType{ + Name: "objectNullKey", + KeyFields: []Field{ + { + Name: "field1", + Kind: StringKind, + Nullable: true, + }, + }, + }, + errContains: "key field \"field1\" cannot be nullable", + }, } for _, tt := range tests { From b9abce42df9bf6da73f96bde2f54a26d0870b73f Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 13:01:59 +0200 Subject: [PATCH 49/79] renaming for clarity --- schema/fields.go | 8 ++++---- schema/fields_test.go | 4 ++-- schema/object_type.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/schema/fields.go b/schema/fields.go index 1500d40bfa2a..9a5cfe7b5095 100644 --- a/schema/fields.go +++ b/schema/fields.go @@ -2,15 +2,15 @@ package schema import "fmt" -// ValidateForKeyFields validates that the value conforms to the set of fields as a Key in an ObjectUpdate. +// ValidateKeyValue validates that the value conforms to the set of fields as a Key in an ObjectUpdate. // See ObjectUpdate.Key for documentation on the requirements of such keys. -func ValidateForKeyFields(keyFields []Field, value interface{}) error { +func ValidateKeyValue(keyFields []Field, value interface{}) error { return validateFieldsValue(keyFields, value) } -// ValidateForValueFields validates that the value conforms to the set of fields as a Value in an ObjectUpdate. +// ValidateValueValue validates that the value conforms to the set of fields as a Value in an ObjectUpdate. // See ObjectUpdate.Value for documentation on the requirements of such values. -func ValidateForValueFields(valueFields []Field, value interface{}) error { +func ValidateValueValue(valueFields []Field, value interface{}) error { valueUpdates, ok := value.(ValueUpdates) if !ok { return validateFieldsValue(valueFields, value) diff --git a/schema/fields_test.go b/schema/fields_test.go index aa22e02a1466..719f6ba21bc9 100644 --- a/schema/fields_test.go +++ b/schema/fields_test.go @@ -56,7 +56,7 @@ func TestValidateForKeyFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateForKeyFields(tt.keyFields, tt.key) + err := ValidateKeyValue(tt.keyFields, tt.key) if tt.errContains == "" { if err != nil { t.Fatalf("unexpected error: %v", err) @@ -128,7 +128,7 @@ func TestValidateForValueFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateForValueFields(tt.valueFields, tt.value) + err := ValidateValueValue(tt.valueFields, tt.value) if tt.errContains == "" { if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/schema/object_type.go b/schema/object_type.go index 89306a07ccb9..280605a9723f 100644 --- a/schema/object_type.go +++ b/schema/object_type.go @@ -73,7 +73,7 @@ func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error { return fmt.Errorf("object type name %q does not match update type name %q", o.Name, update.TypeName) } - if err := ValidateForKeyFields(o.KeyFields, update.Key); err != nil { + if err := ValidateKeyValue(o.KeyFields, update.Key); err != nil { return fmt.Errorf("invalid key for object type %q: %w", update.TypeName, err) } @@ -81,5 +81,5 @@ func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error { return nil } - return ValidateForValueFields(o.ValueFields, update.Value) + return ValidateValueValue(o.ValueFields, update.Value) } From c46600deb3db8d8630f6fbfb377a38f91f47aad1 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 13:03:13 +0200 Subject: [PATCH 50/79] renaming for clarity --- schema/fields.go | 8 ++++---- schema/fields_test.go | 4 ++-- schema/object_type.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/schema/fields.go b/schema/fields.go index 9a5cfe7b5095..be08ca66ef3d 100644 --- a/schema/fields.go +++ b/schema/fields.go @@ -2,15 +2,15 @@ package schema import "fmt" -// ValidateKeyValue validates that the value conforms to the set of fields as a Key in an ObjectUpdate. +// ValidateObjectKey validates that the value conforms to the set of fields as a Key in an ObjectUpdate. // See ObjectUpdate.Key for documentation on the requirements of such keys. -func ValidateKeyValue(keyFields []Field, value interface{}) error { +func ValidateObjectKey(keyFields []Field, value interface{}) error { return validateFieldsValue(keyFields, value) } -// ValidateValueValue validates that the value conforms to the set of fields as a Value in an ObjectUpdate. +// ValidateObjectValue validates that the value conforms to the set of fields as a Value in an ObjectUpdate. // See ObjectUpdate.Value for documentation on the requirements of such values. -func ValidateValueValue(valueFields []Field, value interface{}) error { +func ValidateObjectValue(valueFields []Field, value interface{}) error { valueUpdates, ok := value.(ValueUpdates) if !ok { return validateFieldsValue(valueFields, value) diff --git a/schema/fields_test.go b/schema/fields_test.go index 719f6ba21bc9..befa968657d1 100644 --- a/schema/fields_test.go +++ b/schema/fields_test.go @@ -56,7 +56,7 @@ func TestValidateForKeyFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateKeyValue(tt.keyFields, tt.key) + err := ValidateObjectKey(tt.keyFields, tt.key) if tt.errContains == "" { if err != nil { t.Fatalf("unexpected error: %v", err) @@ -128,7 +128,7 @@ func TestValidateForValueFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateValueValue(tt.valueFields, tt.value) + err := ValidateObjectValue(tt.valueFields, tt.value) if tt.errContains == "" { if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/schema/object_type.go b/schema/object_type.go index 280605a9723f..eff7b8b635a0 100644 --- a/schema/object_type.go +++ b/schema/object_type.go @@ -73,7 +73,7 @@ func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error { return fmt.Errorf("object type name %q does not match update type name %q", o.Name, update.TypeName) } - if err := ValidateKeyValue(o.KeyFields, update.Key); err != nil { + if err := ValidateObjectKey(o.KeyFields, update.Key); err != nil { return fmt.Errorf("invalid key for object type %q: %w", update.TypeName, err) } @@ -81,5 +81,5 @@ func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error { return nil } - return ValidateValueValue(o.ValueFields, update.Value) + return ValidateObjectValue(o.ValueFields, update.Value) } From a00baddb79d2e0b436662102d4b73c583548af82 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 13:11:03 +0200 Subject: [PATCH 51/79] docs, enum compatibility test --- schema/decoder.go | 1 + schema/enum.go | 29 +++++++++++++++++++++- schema/module_schema.go | 47 ++---------------------------------- schema/module_schema_test.go | 33 +++++++++++++++++++++++++ schema/object_type.go | 14 +++++++++++ schema/object_type_test.go | 27 +++++++++++++++++++++ 6 files changed, 105 insertions(+), 46 deletions(-) diff --git a/schema/decoder.go b/schema/decoder.go index 161ac619566f..86aedec9f9dc 100644 --- a/schema/decoder.go +++ b/schema/decoder.go @@ -26,6 +26,7 @@ type ModuleCodec struct { // were decodable to aid debugging. type KVDecoder = func(KVPairUpdate) ([]ObjectUpdate, error) +// KVPairUpdate represents a key-value pair set or delete. type KVPairUpdate struct { // Key is the key of the key-value pair. Key []byte diff --git a/schema/enum.go b/schema/enum.go index d70458d63889..6e0be7c61533 100644 --- a/schema/enum.go +++ b/schema/enum.go @@ -8,7 +8,6 @@ type EnumDefinition struct { // Its name must be unique between all enum types and object types in the module. // The same enum, however, can be used in multiple object types and fields as long as the // definition is identical each time - // TODO: uniqueness validation Name string // Values is a list of distinct, non-empty values that are part of the enum type. @@ -48,3 +47,31 @@ func (e EnumDefinition) ValidateValue(value string) error { } return fmt.Errorf("value %q is not a valid enum value for %s", value, e.Name) } + +// checkEnumCompatibility checks that the enum values are consistent across object types and fields. +func checkEnumCompatibility(enumValueMap map[string]map[string]bool, field Field) error { + if field.Kind != EnumKind { + return nil + } + + enum := field.EnumDefinition + + if existing, ok := enumValueMap[enum.Name]; ok { + if len(existing) != len(enum.Values) { + return fmt.Errorf("enum %q has different number of values in different object types", enum.Name) + } + + for _, value := range enum.Values { + if !existing[value] { + return fmt.Errorf("enum %q has different values in different object types", enum.Name) + } + } + } else { + valueMap := map[string]bool{} + for _, value := range enum.Values { + valueMap[value] = true + } + enumValueMap[enum.Name] = valueMap + } + return nil +} diff --git a/schema/module_schema.go b/schema/module_schema.go index 170288b65023..9412c4456cbe 100644 --- a/schema/module_schema.go +++ b/schema/module_schema.go @@ -10,59 +10,16 @@ type ModuleSchema struct { // Validate validates the module schema. func (s ModuleSchema) Validate() error { - for _, objType := range s.ObjectTypes { - if err := objType.Validate(); err != nil { - return err - } - } - - // validate that shared enum types are consistent across object types enumValueMap := map[string]map[string]bool{} for _, objType := range s.ObjectTypes { - for _, field := range objType.KeyFields { - err := checkEnum(enumValueMap, field) - if err != nil { - return err - } - } - for _, field := range objType.ValueFields { - err := checkEnum(enumValueMap, field) - if err != nil { - return err - } + if err := objType.validate(enumValueMap); err != nil { + return err } } return nil } -func checkEnum(enumValueMap map[string]map[string]bool, field Field) error { - if field.Kind != EnumKind { - return nil - } - - enum := field.EnumDefinition - - if existing, ok := enumValueMap[enum.Name]; ok { - if len(existing) != len(enum.Values) { - return fmt.Errorf("enum %q has different number of values in different object types", enum.Name) - } - - for _, value := range enum.Values { - if !existing[value] { - return fmt.Errorf("enum %q has different values in different object types", enum.Name) - } - } - } else { - valueMap := map[string]bool{} - for _, value := range enum.Values { - valueMap[value] = true - } - enumValueMap[enum.Name] = valueMap - } - return nil -} - // ValidateObjectUpdate validates that the update conforms to the module schema. func (s ModuleSchema) ValidateObjectUpdate(update ObjectUpdate) error { for _, objType := range s.ObjectTypes { diff --git a/schema/module_schema_test.go b/schema/module_schema_test.go index d2b6220a4f2c..d04327811be6 100644 --- a/schema/module_schema_test.go +++ b/schema/module_schema_test.go @@ -110,6 +110,39 @@ func TestModuleSchema_Validate(t *testing.T) { }, errContains: "different values", }, + { + name: "same enum", + moduleSchema: ModuleSchema{ + ObjectTypes: []ObjectType{ + { + Name: "object1", + KeyFields: []Field{ + { + Name: "k", + Kind: EnumKind, + EnumDefinition: EnumDefinition{ + Name: "enum1", + Values: []string{"a", "b"}, + }, + }, + }, + }, + { + Name: "object2", + KeyFields: []Field{ + { + Name: "k", + Kind: EnumKind, + EnumDefinition: EnumDefinition{ + Name: "enum1", + Values: []string{"a", "b"}, + }, + }, + }, + }, + }, + }, + }, } for _, tt := range tests { diff --git a/schema/object_type.go b/schema/object_type.go index eff7b8b635a0..b34b92e538c7 100644 --- a/schema/object_type.go +++ b/schema/object_type.go @@ -29,11 +29,17 @@ type ObjectType struct { // Validate validates the object type. func (o ObjectType) Validate() error { + return o.validate(map[string]map[string]bool{}) +} + +// Validate validates the object type. +func (o ObjectType) validate(enumValueMap map[string]map[string]bool) error { if !ValidateName(o.Name) { return fmt.Errorf("invalid object type name %q", o.Name) } fieldNames := map[string]bool{} + for _, field := range o.KeyFields { if err := field.Validate(); err != nil { return fmt.Errorf("invalid key field %q: %w", field.Name, err) @@ -47,6 +53,10 @@ func (o ObjectType) Validate() error { return fmt.Errorf("duplicate field name %q", field.Name) } fieldNames[field.Name] = true + + if err := checkEnumCompatibility(enumValueMap, field); err != nil { + return err + } } for _, field := range o.ValueFields { @@ -58,6 +68,10 @@ func (o ObjectType) Validate() error { return fmt.Errorf("duplicate field name %q", field.Name) } fieldNames[field.Name] = true + + if err := checkEnumCompatibility(enumValueMap, field); err != nil { + return err + } } if len(o.KeyFields) == 0 && len(o.ValueFields) == 0 { diff --git a/schema/object_type_test.go b/schema/object_type_test.go index e945239f2c67..0a78a371aa96 100644 --- a/schema/object_type_test.go +++ b/schema/object_type_test.go @@ -162,6 +162,33 @@ func TestObjectType_Validate(t *testing.T) { }, errContains: "key field \"field1\" cannot be nullable", }, + { + name: "duplicate incompatible enum", + objectType: ObjectType{ + Name: "objectWithEnums", + KeyFields: []Field{ + { + Name: "key", + Kind: EnumKind, + EnumDefinition: EnumDefinition{ + Name: "enum1", + Values: []string{"a", "b"}, + }, + }, + }, + ValueFields: []Field{ + { + Name: "value", + Kind: EnumKind, + EnumDefinition: EnumDefinition{ + Name: "enum1", + Values: []string{"c", "b"}, + }, + }, + }, + }, + errContains: "enum \"enum1\" has different values", + }, } for _, tt := range tests { From 9cabe6ef71f5cbc6f6aa3aa25781b34e4d65584c Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 13:19:19 +0200 Subject: [PATCH 52/79] add null string validation --- schema/kind.go | 11 +++++++++-- schema/kind_test.go | 2 ++ schema/object_type.go | 3 ++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/schema/kind.go b/schema/kind.go index 9a27eed5cab0..c3fefad52c8a 100644 --- a/schema/kind.go +++ b/schema/kind.go @@ -19,7 +19,6 @@ const ( // StringKind is a string type and values of this type must be of the go type string // containing valid UTF-8 and cannot contain null characters. - // TODO: add validation for null characters StringKind // BytesKind is a bytes type and values of this type must be of the go type []byte. @@ -287,9 +286,17 @@ func (t Kind) ValidateValue(value interface{}) error { switch t { case StringKind: - if !utf8.ValidString(value.(string)) { + str := value.(string) + if !utf8.ValidString(str) { return fmt.Errorf("expected valid utf-8 string, got %s", value) } + + // check for null characters + for _, r := range str { + if r == 0 { + return fmt.Errorf("expected string without null characters, got %s", value) + } + } case IntegerStringKind: if !integerRegex.Match([]byte(value.(string))) { return fmt.Errorf("expected base10 integer, got %s", value) diff --git a/schema/kind_test.go b/schema/kind_test.go index 9027fbb70010..8c2415dfd294 100644 --- a/schema/kind_test.go +++ b/schema/kind_test.go @@ -110,6 +110,8 @@ func TestKind_ValidateValue(t *testing.T) { {Int64Kind, int64(1), true}, {Int32Kind, "abc", false}, {BytesKind, nil, false}, + // strings with null characters are invalid + {StringKind, string([]byte{1, 2, 0, 3}), false}, // check integer, decimal and json more thoroughly {IntegerStringKind, "1", true}, {IntegerStringKind, "0", true}, diff --git a/schema/object_type.go b/schema/object_type.go index b34b92e538c7..9560c5d4e35f 100644 --- a/schema/object_type.go +++ b/schema/object_type.go @@ -32,7 +32,8 @@ func (o ObjectType) Validate() error { return o.validate(map[string]map[string]bool{}) } -// Validate validates the object type. +// validate validates the object type with an enumValueMap that can be +// shared across a whole module schema. func (o ObjectType) validate(enumValueMap map[string]map[string]bool) error { if !ValidateName(o.Name) { return fmt.Errorf("invalid object type name %q", o.Name) From 9e149178c1749b660c91d84ace0a23c3d4eaf784 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 13:21:02 +0200 Subject: [PATCH 53/79] validate UTF-8 --- schema/kind_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/schema/kind_test.go b/schema/kind_test.go index 8c2415dfd294..49799fe4956c 100644 --- a/schema/kind_test.go +++ b/schema/kind_test.go @@ -110,6 +110,8 @@ func TestKind_ValidateValue(t *testing.T) { {Int64Kind, int64(1), true}, {Int32Kind, "abc", false}, {BytesKind, nil, false}, + // string must be valid UTF-8 + {StringKind, string([]byte{0xff, 0xfe, 0xfd}), false}, // strings with null characters are invalid {StringKind, string([]byte{1, 2, 0, 3}), false}, // check integer, decimal and json more thoroughly From 15f95c1681ee97274f875a6e2519e33766309162 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 21:09:56 +0200 Subject: [PATCH 54/79] limit scope, add example tests --- indexer/postgres/count.go | 14 -- indexer/postgres/create_table_test.go | 58 +++++++ indexer/postgres/delete.go | 61 ------- indexer/postgres/enum.go | 54 +++--- indexer/postgres/enum_test.go | 13 ++ indexer/postgres/go.mod | 3 - indexer/postgres/indexer.go | 157 ------------------ indexer/postgres/insert_update.go | 103 ------------ .../internal/testdata/example_schema.go | 62 +++++++ indexer/postgres/module_mgr.go | 3 +- indexer/postgres/params.go | 118 ------------- indexer/postgres/select.go | 97 ----------- indexer/postgres/testing/postgres_test.go | 116 ------------- indexer/postgres/where.go | 60 ------- 14 files changed, 162 insertions(+), 757 deletions(-) delete mode 100644 indexer/postgres/count.go create mode 100644 indexer/postgres/create_table_test.go delete mode 100644 indexer/postgres/delete.go create mode 100644 indexer/postgres/enum_test.go delete mode 100644 indexer/postgres/insert_update.go create mode 100644 indexer/postgres/internal/testdata/example_schema.go delete mode 100644 indexer/postgres/params.go delete mode 100644 indexer/postgres/select.go delete mode 100644 indexer/postgres/testing/postgres_test.go delete mode 100644 indexer/postgres/where.go diff --git a/indexer/postgres/count.go b/indexer/postgres/count.go deleted file mode 100644 index 16f74d627d02..000000000000 --- a/indexer/postgres/count.go +++ /dev/null @@ -1,14 +0,0 @@ -package postgres - -import ( - "context" - "fmt" -) - -// Count returns the number of rows in the table. -func (tm *TableManager) Count(ctx context.Context, conn DBConn) (int, error) { - row := conn.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %q;", tm.TableName())) - var count int - err := row.Scan(&count) - return count, err -} diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go new file mode 100644 index 000000000000..3ddbadb08a8b --- /dev/null +++ b/indexer/postgres/create_table_test.go @@ -0,0 +1,58 @@ +package postgres + +import ( + "os" + + "cosmossdk.io/indexer/postgres/internal/testdata" + "cosmossdk.io/schema" + "cosmossdk.io/schema/logutil" +) + +func exampleCreateTable(objectType schema.ObjectType) { + tm := NewTableManager("test", objectType, Options{Logger: logutil.NoopLogger{}}) + err := tm.CreateTableSql(os.Stdout) + if err != nil { + panic(err) + } +} + +func ExampleCreateTable_AllKinds() { + exampleCreateTable(testdata.AllKindsObject) + // Output: + // CREATE TABLE IF NOT EXISTS "test_all_kinds" ("id" BIGINT NOT NULL, + // "string" TEXT NOT NULL, + // "bytes" BYTEA NOT NULL, + // "int8" SMALLINT NOT NULL, + // "uint8" SMALLINT NOT NULL, + // "int16" SMALLINT NOT NULL, + // "uint16" INTEGER NOT NULL, + // "int32" INTEGER NOT NULL, + // "uint32" BIGINT NOT NULL, + // "int64" BIGINT NOT NULL, + // "uint64" NUMERIC NOT NULL, + // "integer" NUMERIC NOT NULL, + // "decimal" NUMERIC NOT NULL, + // "bool" BOOLEAN NOT NULL, + // "time" TIMESTAMPTZ GENERATED ALWAYS AS (nanos_to_timestamptz("time_nanos")) STORED, + // "time_nanos" BIGINT NOT NULL, + // "duration" BIGINT NOT NULL, + // "float32" REAL NOT NULL, + // "float64" DOUBLE PRECISION NOT NULL, + // "bech32address" TEXT NOT NULL, + // "enum" "test_my_enum" NOT NULL, + // "json" JSONB NOT NULL, + // PRIMARY KEY ("id") + // ); + // GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; +} + +func ExampleCreateTable_Singleton() { + exampleCreateTable(testdata.SingletonObject) + // Output: + // CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), + // "foo" TEXT NOT NULL, + // "bar" INTEGER NOT NULL, + // PRIMARY KEY (_id) + // ); + // GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; +} diff --git a/indexer/postgres/delete.go b/indexer/postgres/delete.go deleted file mode 100644 index 317e5db19af8..000000000000 --- a/indexer/postgres/delete.go +++ /dev/null @@ -1,61 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "io" - "strings" -) - -// Delete deletes the row with the provided key from the table. -func (tm *TableManager) Delete(ctx context.Context, conn DBConn, key interface{}) error { - buf := new(strings.Builder) - var params []interface{} - var err error - if tm.options.RetainDeletions && tm.typ.RetainDeletions { - params, err = tm.RetainDeleteSqlAndParams(buf, key) - } else { - params, err = tm.DeleteSqlAndParams(buf, key) - } - if err != nil { - return err - } - - sqlStr := buf.String() - tm.options.Logger.Debug("Delete", "sql", sqlStr, "params", params) - _, err = conn.ExecContext(ctx, sqlStr, params...) - return err -} - -// DeleteSqlAndParams generates a DELETE statement and binding parameters for the provided key. -func (tm *TableManager) DeleteSqlAndParams(w io.Writer, key interface{}) ([]interface{}, error) { - _, err := fmt.Fprintf(w, "DELETE FROM %q", tm.TableName()) - if err != nil { - return nil, err - } - - _, keyParams, err := tm.WhereSqlAndParams(w, key, 1) - if err != nil { - return nil, err - } - - _, err = fmt.Fprintf(w, ";") - return keyParams, err -} - -// RetainDeleteSqlAndParams generates an UPDATE statement to set the _deleted column to true for the provided key -// which is used when the table is set to retain deletions mode. -func (tm *TableManager) RetainDeleteSqlAndParams(w io.Writer, key interface{}) ([]interface{}, error) { - _, err := fmt.Fprintf(w, "UPDATE %q SET _deleted = TRUE", tm.TableName()) - if err != nil { - return nil, err - } - - _, keyParams, err := tm.WhereSqlAndParams(w, key, 1) - if err != nil { - return nil, err - } - - _, err = fmt.Fprintf(w, ";") - return keyParams, err -} diff --git a/indexer/postgres/enum.go b/indexer/postgres/enum.go index 4295a4bd819d..e846452592e0 100644 --- a/indexer/postgres/enum.go +++ b/indexer/postgres/enum.go @@ -10,30 +10,6 @@ import ( "cosmossdk.io/schema" ) -// createEnumTypesForFields creates enum types for all the fields that have enum kind in the module schema. -func (m *ModuleManager) createEnumTypesForFields(ctx context.Context, conn DBConn, fields []schema.Field) error { - for _, field := range fields { - if field.Kind != schema.EnumKind { - continue - } - - if _, ok := m.definedEnums[field.EnumDefinition.Name]; ok { - // if the enum type is already defined, skip - // we assume validation already happened - continue - } - - err := m.CreateEnumType(ctx, conn, field.EnumDefinition) - if err != nil { - return err - } - - m.definedEnums[field.EnumDefinition.Name] = field.EnumDefinition - } - - return nil -} - // CreateEnumType creates an enum type in the database. func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum schema.EnumDefinition) error { typeName := enumTypeName(m.moduleName, enum) @@ -49,7 +25,7 @@ func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum sc } buf := new(strings.Builder) - err := m.CreateEnumTypeSql(buf, enum) + err := CreateEnumTypeSql(buf, m.moduleName, enum) if err != nil { return err } @@ -61,8 +37,8 @@ func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum sc } // CreateEnumTypeSql generates a CREATE TYPE statement for the enum definition. -func (m *ModuleManager) CreateEnumTypeSql(writer io.Writer, enum schema.EnumDefinition) error { - _, err := fmt.Fprintf(writer, "CREATE TYPE %q AS ENUM (", enumTypeName(m.moduleName, enum)) +func CreateEnumTypeSql(writer io.Writer, moduleName string, enum schema.EnumDefinition) error { + _, err := fmt.Fprintf(writer, "CREATE TYPE %q AS ENUM (", enumTypeName(moduleName, enum)) if err != nil { return err } @@ -88,3 +64,27 @@ func (m *ModuleManager) CreateEnumTypeSql(writer io.Writer, enum schema.EnumDefi func enumTypeName(moduleName string, enum schema.EnumDefinition) string { return fmt.Sprintf("%s_%s", moduleName, enum.Name) } + +// createEnumTypesForFields creates enum types for all the fields that have enum kind in the module schema. +func (m *ModuleManager) createEnumTypesForFields(ctx context.Context, conn DBConn, fields []schema.Field) error { + for _, field := range fields { + if field.Kind != schema.EnumKind { + continue + } + + if _, ok := m.definedEnums[field.EnumDefinition.Name]; ok { + // if the enum type is already defined, skip + // we assume validation already happened + continue + } + + err := m.CreateEnumType(ctx, conn, field.EnumDefinition) + if err != nil { + return err + } + + m.definedEnums[field.EnumDefinition.Name] = field.EnumDefinition + } + + return nil +} diff --git a/indexer/postgres/enum_test.go b/indexer/postgres/enum_test.go new file mode 100644 index 000000000000..abae7f252001 --- /dev/null +++ b/indexer/postgres/enum_test.go @@ -0,0 +1,13 @@ +package postgres + +import ( + "os" + + "cosmossdk.io/indexer/postgres/internal/testdata" +) + +func ExampleCreateEnumType() { + CreateEnumTypeSql(os.Stdout, "test", testdata.MyEnum) + // Output: + // CREATE TYPE "test_my_enum" AS ENUM ('a', 'b', 'c'); +} diff --git a/indexer/postgres/go.mod b/indexer/postgres/go.mod index dc0615cf41b6..acac6324b497 100644 --- a/indexer/postgres/go.mod +++ b/indexer/postgres/go.mod @@ -10,7 +10,4 @@ go 1.12 // and cosmossdk.io/indexer/base. require cosmossdk.io/schema v0.0.0 -// TODO: is this dependency okay? -require github.com/cosmos/btcutil v1.0.5 - replace cosmossdk.io/schema => ../../schema diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index fc67c1209beb..179bbf23f3cd 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -1,167 +1,10 @@ package postgres import ( - "context" - "database/sql" - "encoding/json" - "fmt" - - "cosmossdk.io/schema/appdata" - "cosmossdk.io/schema/indexing" "cosmossdk.io/schema/logutil" ) -type Indexer struct { - ctx context.Context - db *sql.DB - tx *sql.Tx - options Options - - modules map[string]*ModuleManager -} - -func (i *Indexer) Initialize(ctx context.Context, data indexing.InitializationData) (indexing.InitializationResult, error) { - i.options.Logger.Info("Starting Postgres Indexer") - - go func() { - <-ctx.Done() - err := i.db.Close() - if err != nil { - panic(fmt.Sprintf("failed to close database: %v", err)) - } - }() - - i.ctx = ctx - - tx, err := i.db.BeginTx(ctx, nil) - if err != nil { - return indexing.InitializationResult{}, fmt.Errorf("failed to start transaction: %w", err) - } - - _, err = tx.ExecContext(ctx, BaseSQL) - if err != nil { - return indexing.InitializationResult{}, err - } - - i.tx = tx - - return indexing.InitializationResult{ - Listener: i.listener(), - }, nil -} - -type configOptions struct { - DatabaseDriver string `json:"database_driver"` - DatabaseURL string `json:"database_url"` - // TODO should probably default to true - RetainDeletions bool `json:"retain_deletions"` -} - -func init() { - indexing.RegisterIndexer("postgres", func(rawOpts map[string]interface{}, resources indexing.IndexerResources) (indexing.Indexer, error) { - bz, err := json.Marshal(rawOpts) - if err != nil { - return nil, fmt.Errorf("failed to marshal options: %w", err) - } - - var opts configOptions - err = json.Unmarshal(bz, &opts) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal options: %w", err) - } - - if opts.DatabaseDriver == "" { - opts.DatabaseDriver = "pgx" - } - - if opts.DatabaseURL == "" { - return nil, fmt.Errorf("connection URL not set") - } - - db, err := sql.Open(opts.DatabaseDriver, opts.DatabaseURL) - if err != nil { - return nil, fmt.Errorf("failed to open database: %w", err) - } - - return NewIndexer(db, Options{ - RetainDeletions: opts.RetainDeletions, - Logger: resources.Logger, - }) - }) -} - type Options struct { RetainDeletions bool Logger logutil.Logger } - -func NewIndexer(db *sql.DB, opts Options) (*Indexer, error) { - return &Indexer{ - db: db, - modules: map[string]*ModuleManager{}, - options: opts, - }, nil -} - -func (i *Indexer) listener() appdata.Listener { - return appdata.Listener{ - InitializeModuleData: i.initModuleSchema, - OnObjectUpdate: i.onObjectUpdate, - Commit: i.commit, - } -} - -func (i *Indexer) initModuleSchema(data appdata.ModuleInitializationData) error { - moduleName := data.ModuleName - modSchema := data.Schema - _, ok := i.modules[moduleName] - if ok { - return fmt.Errorf("module %s already initialized", moduleName) - } - - mm := newModuleManager(moduleName, modSchema, i.options) - i.modules[moduleName] = mm - - return mm.InitializeSchema(i.ctx, i.tx) -} - -func (i *Indexer) onObjectUpdate(data appdata.ObjectUpdateData) error { - module := data.ModuleName - mod, ok := i.modules[module] - if !ok { - return fmt.Errorf("module %s not initialized", module) - } - - for _, update := range data.Updates { - tm, ok := mod.tables[update.TypeName] - if !ok { - return fmt.Errorf("object type %s not found in schema for module %s", update.TypeName, module) - } - - var err error - if update.Delete { - err = tm.Delete(i.ctx, i.tx, update.Key) - } else { - err = tm.InsertUpdate(i.ctx, i.tx, update.Key, update.Value) - } - if err != nil { - return err - } - } - return nil -} - -func (i *Indexer) commit(_ appdata.CommitData) error { - err := i.tx.Commit() - if err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) - } - - i.tx, err = i.db.BeginTx(i.ctx, nil) - return err -} - -// Modules retains the module managers for the indexer. -func (i *Indexer) Modules() map[string]*ModuleManager { - return i.modules -} diff --git a/indexer/postgres/insert_update.go b/indexer/postgres/insert_update.go deleted file mode 100644 index cc68cb219cc9..000000000000 --- a/indexer/postgres/insert_update.go +++ /dev/null @@ -1,103 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "io" - "strings" -) - -// InsertUpdate inserts or updates the row with the provided key and value. -func (tm *TableManager) InsertUpdate(ctx context.Context, conn DBConn, key, value interface{}) error { - exists, err := tm.Exists(ctx, conn, key) - if err != nil { - return err - } - - buf := new(strings.Builder) - var params []interface{} - if exists { - params, err = tm.UpdateSql(buf, key, value) - } else { - params, err = tm.InsertSql(buf, key, value) - } - - sqlStr := buf.String() - tm.options.Logger.Debug("Insert or Update", "sql", sqlStr, "params", params) - _, err = conn.ExecContext(ctx, sqlStr, params...) - return err -} - -// InsertSql generates an INSERT statement and binding parameters for the provided key and value. -func (tm *TableManager) InsertSql(w io.Writer, key, value interface{}) ([]interface{}, error) { - keyParams, keyCols, err := tm.bindKeyParams(key) - if err != nil { - return nil, err - } - - valueParams, valueCols, err := tm.bindValueParams(value) - if err != nil { - return nil, err - } - - var allParams []interface{} - allParams = append(allParams, keyParams...) - allParams = append(allParams, valueParams...) - - allCols := make([]string, 0, len(keyCols)+len(valueCols)) - allCols = append(allCols, keyCols...) - allCols = append(allCols, valueCols...) - - var paramBindings []string - for i := 1; i <= len(allCols); i++ { - paramBindings = append(paramBindings, fmt.Sprintf("$%d", i)) - } - - _, err = fmt.Fprintf(w, "INSERT INTO %q (%s) VALUES (%s);", tm.TableName(), - strings.Join(allCols, ", "), - strings.Join(paramBindings, ", "), - ) - return allParams, err -} - -// UpdateSql generates an UPDATE statement and binding parameters for the provided key and value. -func (tm *TableManager) UpdateSql(w io.Writer, key, value interface{}) ([]interface{}, error) { - _, err := fmt.Fprintf(w, "UPDATE %q SET ", tm.TableName()) - - valueParams, valueCols, err := tm.bindValueParams(value) - if err != nil { - return nil, err - } - - paramIdx := 1 - for i, col := range valueCols { - if i > 0 { - _, err = fmt.Fprintf(w, ", ") - if err != nil { - return nil, err - } - } - _, err = fmt.Fprintf(w, "%s = $%d", col, paramIdx) - if err != nil { - return nil, err - } - - paramIdx++ - } - - if tm.options.RetainDeletions && tm.typ.RetainDeletions { - _, err = fmt.Fprintf(w, ", _deleted = FALSE") - if err != nil { - return nil, err - } - } - - _, keyParams, err := tm.WhereSqlAndParams(w, key, paramIdx) - if err != nil { - return nil, err - } - - allParams := append(valueParams, keyParams...) - _, err = fmt.Fprintf(w, ";") - return allParams, err -} diff --git a/indexer/postgres/internal/testdata/example_schema.go b/indexer/postgres/internal/testdata/example_schema.go new file mode 100644 index 000000000000..764ecc3f04d1 --- /dev/null +++ b/indexer/postgres/internal/testdata/example_schema.go @@ -0,0 +1,62 @@ +package testdata + +import "cosmossdk.io/schema" + +var ExampleSchema schema.ModuleSchema + +var AllKindsObject schema.ObjectType + +func init() { + AllKindsObject = schema.ObjectType{ + Name: "all_kinds", + KeyFields: []schema.Field{ + { + Name: "id", + Kind: schema.Int64Kind, + }, + }, + } + + for i := schema.InvalidKind + 1; i <= schema.MAX_VALID_KIND; i++ { + field := schema.Field{ + Name: i.String(), + Kind: i, + } + + switch i { + case schema.EnumKind: + field.EnumDefinition = MyEnum + case schema.Bech32AddressKind: + field.AddressPrefix = "foo" + default: + } + + AllKindsObject.ValueFields = append(AllKindsObject.ValueFields, field) + } + + ExampleSchema = schema.ModuleSchema{ + ObjectTypes: []schema.ObjectType{ + AllKindsObject, + SingletonObject, + }, + } +} + +var SingletonObject = schema.ObjectType{ + Name: "singleton", + ValueFields: []schema.Field{ + { + Name: "foo", + Kind: schema.StringKind, + }, + { + Name: "bar", + Kind: schema.Int32Kind, + }, + }, +} + +var MyEnum = schema.EnumDefinition{ + Name: "my_enum", + Values: []string{"a", "b", "c"}, +} diff --git a/indexer/postgres/module_mgr.go b/indexer/postgres/module_mgr.go index fa907bf5b16e..de92d84f7f1f 100644 --- a/indexer/postgres/module_mgr.go +++ b/indexer/postgres/module_mgr.go @@ -16,7 +16,8 @@ type ModuleManager struct { options Options } -func newModuleManager(moduleName string, modSchema schema.ModuleSchema, options Options) *ModuleManager { +// NewModuleManager creates a new ModuleManager for the given module schema. +func NewModuleManager(moduleName string, modSchema schema.ModuleSchema, options Options) *ModuleManager { return &ModuleManager{ moduleName: moduleName, schema: modSchema, diff --git a/indexer/postgres/params.go b/indexer/postgres/params.go deleted file mode 100644 index 3ef4352b6b95..000000000000 --- a/indexer/postgres/params.go +++ /dev/null @@ -1,118 +0,0 @@ -package postgres - -import ( - "fmt" - "time" - - "github.com/cosmos/btcutil/bech32" - - "cosmossdk.io/schema" -) - -// bindKeyParams binds the key to the key columns. -func (tm *TableManager) bindKeyParams(key interface{}) ([]interface{}, []string, error) { - n := len(tm.typ.KeyFields) - if n == 0 { - // singleton, set _id = 1 - return []interface{}{1}, []string{"_id"}, nil - } else if n == 1 { - return tm.bindParams(tm.typ.KeyFields, []interface{}{key}) - } else { - key, ok := key.([]interface{}) - if !ok { - return nil, nil, fmt.Errorf("expected key to be a slice") - } - - return tm.bindParams(tm.typ.KeyFields, key) - } -} - -func (tm *TableManager) bindValueParams(value interface{}) (params []interface{}, valueCols []string, err error) { - n := len(tm.typ.ValueFields) - if n == 0 { - return nil, nil, nil - } else if valueUpdates, ok := value.(schema.ValueUpdates); ok { - var e error - var fields []schema.Field - var params []interface{} - if err := valueUpdates.Iterate(func(name string, value interface{}) bool { - field, ok := tm.valueFields[name] - if !ok { - e = fmt.Errorf("unknown column %q", name) - return false - } - fields = append(fields, field) - params = append(params, value) - return true - }); err != nil { - return nil, nil, err - } - if e != nil { - return nil, nil, e - } - - return tm.bindParams(fields, params) - } else if n == 1 { - return tm.bindParams(tm.typ.ValueFields, []interface{}{value}) - } else { - values, ok := value.([]interface{}) - if !ok { - return nil, nil, fmt.Errorf("expected values to be a slice") - } - - return tm.bindParams(tm.typ.ValueFields, values) - } -} - -func (tm *TableManager) bindParams(fields []schema.Field, values []interface{}) ([]interface{}, []string, error) { - names := make([]string, 0, len(fields)) - params := make([]interface{}, 0, len(fields)) - for i, field := range fields { - if i >= len(values) { - return nil, nil, fmt.Errorf("missing value for field %q", field.Name) - } - - param, err := tm.bindParam(field, values[i]) - if err != nil { - return nil, nil, err - } - - name, err := tm.updatableColumnName(field) - if err != nil { - return nil, nil, err - } - - names = append(names, name) - params = append(params, param) - } - return params, names, nil -} - -func (tm *TableManager) bindParam(field schema.Field, value interface{}) (param interface{}, err error) { - param = value - if value == nil { - if !field.Nullable { - return nil, fmt.Errorf("expected non-null value for field %q", field.Name) - } - } else if field.Kind == schema.TimeKind { - t, ok := value.(time.Time) - if !ok { - return nil, fmt.Errorf("expected time.Time value for field %q, got %T", field.Name, value) - } - - param = t.UnixNano() - } else if field.Kind == schema.DurationKind { - t, ok := value.(time.Duration) - if !ok { - return nil, fmt.Errorf("expected time.Duration value for field %q, got %T", field.Name, value) - } - - param = int64(t) - } else if field.Kind == schema.Bech32AddressKind { - param, err = bech32.EncodeFromBase256(field.AddressPrefix, value.([]byte)) - if err != nil { - return nil, fmt.Errorf("encoding bech32 failed: %w", err) - } - } - return -} diff --git a/indexer/postgres/select.go b/indexer/postgres/select.go deleted file mode 100644 index 830a628566b3..000000000000 --- a/indexer/postgres/select.go +++ /dev/null @@ -1,97 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "fmt" - "io" - "strings" -) - -// Exists checks if a row with the provided key exists in the table. -func (tm *TableManager) Exists(ctx context.Context, conn DBConn, key interface{}) (bool, error) { - buf := new(strings.Builder) - params, err := tm.ExistsSqlAndParams(buf, key) - if err != nil { - return false, err - } - - return tm.checkExists(ctx, conn, buf.String(), params) -} - -// ExistsSqlAndParams generates a SELECT statement to check if a row with the provided key exists in the table. -func (tm *TableManager) ExistsSqlAndParams(w io.Writer, key interface{}) ([]interface{}, error) { - _, err := fmt.Fprintf(w, "SELECT 1 FROM %q", tm.TableName()) - if err != nil { - return nil, err - } - - _, keyParams, err := tm.WhereSqlAndParams(w, key, 1) - if err != nil { - return nil, err - } - - _, err = fmt.Fprintf(w, ";") - return keyParams, err -} - -// Equals checks if a row with the provided key and value exists. -func (tm *TableManager) Equals(ctx context.Context, conn DBConn, key, val interface{}) (bool, error) { - buf := new(strings.Builder) - params, err := tm.EqualsSqlAndParams(buf, key, val) - if err != nil { - return false, err - } - - return tm.checkExists(ctx, conn, buf.String(), params) -} - -// EqualsSqlAndParams generates a SELECT statement to check if a row with the provided key and value exists in the table. -func (tm *TableManager) EqualsSqlAndParams(w io.Writer, key, val interface{}) ([]interface{}, error) { - _, err := fmt.Fprintf(w, "SELECT 1 FROM %q", tm.TableName()) - if err != nil { - return nil, err - } - - keyParams, keyCols, err := tm.bindKeyParams(key) - if err != nil { - return nil, err - } - - valueParams, valueCols, err := tm.bindValueParams(val) - if err != nil { - return nil, err - } - - allParams := make([]interface{}, 0, len(keyParams)+len(valueParams)) - allParams = append(allParams, keyParams...) - allParams = append(allParams, valueParams...) - - allCols := make([]string, 0, len(keyCols)+len(valueCols)) - allCols = append(allCols, keyCols...) - allCols = append(allCols, valueCols...) - - _, allParams, err = tm.WhereSql(w, allParams, allCols, 1) - if err != nil { - return nil, err - } - - _, err = fmt.Fprintf(w, ";") - return allParams, err -} - -// checkExists checks if a row exists in the table. -func (tm *TableManager) checkExists(ctx context.Context, conn DBConn, sqlStr string, params []interface{}) (bool, error) { - tm.options.Logger.Debug("Select", "sql", sqlStr, "params", params) - var res interface{} - // TODO check for multiple rows which would be a logic error - err := conn.QueryRowContext(ctx, sqlStr, params...).Scan(&res) - switch err { - case nil: - return true, nil - case sql.ErrNoRows: - return false, nil - default: - return false, err - } -} diff --git a/indexer/postgres/testing/postgres_test.go b/indexer/postgres/testing/postgres_test.go deleted file mode 100644 index 7d59d76804b3..000000000000 --- a/indexer/postgres/testing/postgres_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package testing - -import ( - "context" - "database/sql" - "fmt" - "os" - "testing" - - "cosmossdk.io/log" - embeddedpostgres "github.com/fergusstrange/embedded-postgres" - "github.com/hashicorp/consul/sdk/freeport" - _ "github.com/jackc/pgx/v5/stdlib" - "github.com/stretchr/testify/require" - - "cosmossdk.io/indexer/postgres" - "cosmossdk.io/schema" - "cosmossdk.io/schema/appdata" - "cosmossdk.io/schema/indexing" - indexertesting "cosmossdk.io/schema/testing" - "cosmossdk.io/schema/testing/appdatasim" - "cosmossdk.io/schema/testing/statesim" -) - -func TestPostgresIndexer(t *testing.T) { - t.Run("RetainDeletions", func(t *testing.T) { - testPostgresIndexer(t, true) - }) - t.Run("NoRetainDeletions", func(t *testing.T) { - testPostgresIndexer(t, false) - }) -} - -func testPostgresIndexer(t *testing.T, retainDeletions bool) { - tempDir, err := os.MkdirTemp("", "postgres-indexer-test") - require.NoError(t, err) - - dbPort := freeport.GetOne(t) - pgConfig := embeddedpostgres.DefaultConfig(). - Port(uint32(dbPort)). - DataPath(tempDir) - - dbUrl := pgConfig.GetConnectionURL() - pg := embeddedpostgres.NewDatabase(pgConfig) - require.NoError(t, pg.Start()) - - ctx, cancel := context.WithCancel(context.Background()) - - t.Cleanup(func() { - cancel() - require.NoError(t, pg.Stop()) - err := os.RemoveAll(tempDir) - require.NoError(t, err) - }) - - db, err := sql.Open("pgx", dbUrl) - require.NoError(t, err) - - indexer, err := postgres.NewIndexer(db, postgres.Options{ - RetainDeletions: retainDeletions, - Logger: log.NewTestLogger(t), - }) - require.NoError(t, err) - - res, err := indexer.Initialize(ctx, indexing.InitializationData{}) - require.NoError(t, err) - - fixture := appdatasim.NewSimulator(appdatasim.Options{ - Listener: appdata.ListenerMux( - appdata.DebugListener(os.Stdout), - res.Listener, - ), - AppSchema: indexertesting.ExampleAppSchema, - StateSimOptions: statesim.Options{ - CanRetainDeletions: retainDeletions, - }, - }) - - require.NoError(t, fixture.Initialize()) - - blockDataGen := fixture.BlockDataGenN(1000) - for i := 0; i < 1000; i++ { - blockData := blockDataGen.Example(i) - require.NoError(t, fixture.ProcessBlockData(blockData)) - - require.NoError(t, fixture.AppState().ScanModules(func(moduleName string, mod *statesim.Module) error { - modMgr, ok := indexer.Modules()[moduleName] - require.True(t, ok) - - return mod.ScanObjectCollections(func(collection *statesim.ObjectCollection) error { - tblMgr, ok := modMgr.Tables()[collection.ObjectType().Name] - require.True(t, ok) - - expectedCount := collection.Len() - actualCount, err := tblMgr.Count(context.Background(), db) - require.NoError(t, err) - require.Equalf(t, expectedCount, actualCount, "table %s %s count mismatch", moduleName, collection.ObjectType().Name) - - return collection.ScanState(func(update schema.ObjectUpdate) error { - found, err := tblMgr.Equals( - context.Background(), - db, update.Key, update.Value) - if err != nil { - return err - } - - if !found { - return fmt.Errorf("object not found in table %s %s %v %v", moduleName, collection.ObjectType().Name, update.Key, update.Value) - } - - return nil - }) - }) - })) - } -} diff --git a/indexer/postgres/where.go b/indexer/postgres/where.go deleted file mode 100644 index 3f10838829a1..000000000000 --- a/indexer/postgres/where.go +++ /dev/null @@ -1,60 +0,0 @@ -package postgres - -import ( - "fmt" - "io" -) - -// WhereSqlAndParams generates a WHERE clause for the provided key and returns the parameters. -func (tm *TableManager) WhereSqlAndParams(w io.Writer, key interface{}, startParamIdx int) (endParamIdx int, keyParams []interface{}, err error) { - var keyCols []string - keyParams, keyCols, err = tm.bindKeyParams(key) - if err != nil { - return - } - - endParamIdx, keyParams, err = tm.WhereSql(w, keyParams, keyCols, startParamIdx) - return -} - -// WhereSql generates a WHERE clause for the provided columns and returns the parameters. -func (tm *TableManager) WhereSql(w io.Writer, params []interface{}, cols []string, startParamIdx int) (endParamIdx int, resParams []interface{}, err error) { - _, err = fmt.Fprintf(w, " WHERE ") - if err != nil { - return - } - - endParamIdx = startParamIdx - for i, col := range cols { - if i > 0 { - _, err = fmt.Fprintf(w, " AND ") - if err != nil { - return - } - } - - _, err = fmt.Fprintf(w, "%s ", col) - if err != nil { - return - } - - if params[i] == nil { - _, err = fmt.Fprintf(w, "IS NULL") - if err != nil { - return - } - - } else { - _, err = fmt.Fprintf(w, "= $%d", endParamIdx) - if err != nil { - return - } - - resParams = append(resParams, params[i]) - - endParamIdx++ - } - } - - return -} From cf36f83803797130872013880c5421a8843e8cd1 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 21:11:20 +0200 Subject: [PATCH 55/79] limit PR scope --- schema/appdata/README.md | 6 +- schema/appdata/async.go | 115 ------------ schema/appdata/data.go | 99 ---------- schema/appdata/debug.go | 31 --- schema/appdata/listener.go | 122 +++++++++--- schema/appdata/mux.go | 121 ------------ schema/appdata/packet.go | 58 ------ schema/decoder.go | 25 +-- schema/enum.go | 31 --- schema/field.go | 3 +- schema/fields.go | 8 +- schema/fields_test.go | 4 +- schema/indexing/indexer.go | 36 ---- schema/kind.go | 38 ++-- schema/kind_test.go | 104 +++++----- schema/module_schema.go | 47 ++++- schema/module_schema_test.go | 33 ---- schema/object_type.go | 25 +-- schema/object_type_test.go | 41 ---- schema/testing/CHANGELOG.md | 37 ---- schema/testing/README.md | 3 - schema/testing/app.go | 21 --- schema/testing/app_test.go | 18 -- schema/testing/appdatasim/app_data.go | 153 --------------- schema/testing/appdatasim/app_data_test.go | 30 --- schema/testing/appdatasim/doc.go | 2 - .../testdata/app_sim_example_schema.txt | 91 --------- schema/testing/appdatasim/write_listener.go | 43 ----- schema/testing/doc.go | 3 - schema/testing/enum.go | 19 -- schema/testing/enum_test.go | 15 -- schema/testing/example_schema.go | 176 ----------------- schema/testing/field.go | 177 ------------------ schema/testing/field_test.go | 37 ---- schema/testing/go.mod | 20 -- schema/testing/go.sum | 18 -- schema/testing/module_schema.go | 32 ---- schema/testing/module_schema_test.go | 15 -- schema/testing/name.go | 10 - schema/testing/name_test.go | 17 -- schema/testing/object.go | 128 ------------- schema/testing/object_test.go | 31 --- schema/testing/statesim/app.go | 93 --------- schema/testing/statesim/doc.go | 3 - schema/testing/statesim/module.go | 82 -------- schema/testing/statesim/object.go | 127 ------------- schema/testing/statesim/options.go | 5 - 47 files changed, 222 insertions(+), 2131 deletions(-) delete mode 100644 schema/appdata/async.go delete mode 100644 schema/appdata/data.go delete mode 100644 schema/appdata/debug.go delete mode 100644 schema/appdata/mux.go delete mode 100644 schema/appdata/packet.go delete mode 100644 schema/indexing/indexer.go delete mode 100644 schema/testing/CHANGELOG.md delete mode 100644 schema/testing/README.md delete mode 100644 schema/testing/app.go delete mode 100644 schema/testing/app_test.go delete mode 100644 schema/testing/appdatasim/app_data.go delete mode 100644 schema/testing/appdatasim/app_data_test.go delete mode 100644 schema/testing/appdatasim/doc.go delete mode 100644 schema/testing/appdatasim/testdata/app_sim_example_schema.txt delete mode 100644 schema/testing/appdatasim/write_listener.go delete mode 100644 schema/testing/doc.go delete mode 100644 schema/testing/enum.go delete mode 100644 schema/testing/enum_test.go delete mode 100644 schema/testing/example_schema.go delete mode 100644 schema/testing/field.go delete mode 100644 schema/testing/field_test.go delete mode 100644 schema/testing/go.mod delete mode 100644 schema/testing/go.sum delete mode 100644 schema/testing/module_schema.go delete mode 100644 schema/testing/module_schema_test.go delete mode 100644 schema/testing/name.go delete mode 100644 schema/testing/name_test.go delete mode 100644 schema/testing/object.go delete mode 100644 schema/testing/object_test.go delete mode 100644 schema/testing/statesim/app.go delete mode 100644 schema/testing/statesim/doc.go delete mode 100644 schema/testing/statesim/module.go delete mode 100644 schema/testing/statesim/object.go delete mode 100644 schema/testing/statesim/options.go diff --git a/schema/appdata/README.md b/schema/appdata/README.md index d750e42efa56..f6a3e6766373 100644 --- a/schema/appdata/README.md +++ b/schema/appdata/README.md @@ -1,6 +1,6 @@ -# Listener +# App Data -The `listener` package defines the basic types for streaming blockchain event and state data to external listeners, with a specific focus on supporting logical decoding and indexing of state. +The `appdata` package defines the basic types for streaming blockchain event and state data to external listeners, with a specific focus on supporting logical decoding and indexing of state. A blockchain data source should accept a `Listener` instance and invoke the provided callbacks in the correct order. A downstream listener should provide a `Listener` instance and perform operations based on the data passed to its callbacks. @@ -29,4 +29,4 @@ sequenceDiagram Sources will generally only call `InitializeModuleSchema` and `OnObjectUpdate` if they have native logical decoding capabilities. Usually, the indexer framework will provide this functionality based on `OnKVPair` data and `schema.HasModuleCodec` implementations. -`StartBlock` and `OnBlockHeader` should be called only once at the beginning of a block, and `Commit` should be called only once at the end of a block. The `OnTx`, `OnEvent`, `OnKVPair` and `OnObjectUpdate` must be called after `OnBlockHeader`, may be called multiple times within a block and indexers should not assume that the order is logical unless `InitializationData.HasEventAlignedWrites` is true. +`StartBlock` and `OnBlockHeader` should be called only once at the beginning of a block, and `Commit` should be called only once at the end of a block. The `OnTx`, `OnEvent`, `OnKVPair` and `OnObjectUpdate` must be called after `OnBlockHeader`, may be called multiple times within a block and indexers should not assume that the order is logical unless `InitializationData.HasEventAlignedWrites` is true. diff --git a/schema/appdata/async.go b/schema/appdata/async.go deleted file mode 100644 index 47ffc707fe54..000000000000 --- a/schema/appdata/async.go +++ /dev/null @@ -1,115 +0,0 @@ -package appdata - -func AsyncListener(listener Listener, bufferSize int, commitChan chan<- error, doneChan <-chan struct{}) Listener { - packetChan := make(chan Packet, bufferSize) - res := Listener{} - - go func() { - var err error - for { - select { - case packet := <-packetChan: - if err != nil { - // if we have an error, don't process any more packets - // and return the error and finish when it's time to commit - if _, ok := packet.(CommitData); ok { - commitChan <- err - return - } - } else { - // process the packet - err = listener.SendPacket(packet) - // if it's a commit - if _, ok := packet.(CommitData); ok { - commitChan <- err - if err != nil { - return - } - } - } - - case <-doneChan: - return - } - } - }() - - if listener.InitializeModuleData != nil { - res.InitializeModuleData = func(data ModuleInitializationData) error { - packetChan <- data - return nil - } - } - - if listener.StartBlock != nil { - res.StartBlock = func(data StartBlockData) error { - packetChan <- data - return nil - } - } - - if listener.OnTx != nil { - res.OnTx = func(data TxData) error { - packetChan <- data - return nil - } - } - - if listener.OnEvent != nil { - res.OnEvent = func(data EventData) error { - packetChan <- data - return nil - } - } - - if listener.OnKVPair != nil { - res.OnKVPair = func(data KVPairData) error { - packetChan <- data - return nil - } - } - - if listener.OnObjectUpdate != nil { - res.OnObjectUpdate = func(data ObjectUpdateData) error { - packetChan <- data - return nil - } - } - - if listener.Commit != nil { - res.Commit = func(data CommitData) error { - packetChan <- data - return nil - } - } - - return res -} - -func AsyncListenerMux(listeners []Listener, bufferSize int, doneChan <-chan struct{}) Listener { - asyncListeners := make([]Listener, len(listeners)) - commitChans := make([]chan error, len(listeners)) - for i, l := range listeners { - commitChan := make(chan error) - commitChans[i] = commitChan - asyncListeners[i] = AsyncListener(l, bufferSize, commitChan, doneChan) - } - mux := ListenerMux(asyncListeners...) - muxCommit := mux.Commit - mux.Commit = func(data CommitData) error { - err := muxCommit(data) - if err != nil { - return err - } - - for _, commitChan := range commitChans { - err := <-commitChan - if err != nil { - return err - } - } - return nil - } - - return mux -} diff --git a/schema/appdata/data.go b/schema/appdata/data.go deleted file mode 100644 index 1712c623b5fa..000000000000 --- a/schema/appdata/data.go +++ /dev/null @@ -1,99 +0,0 @@ -package appdata - -import ( - "encoding/json" - - "cosmossdk.io/schema" -) - -// InitializationData represents initialization data that is passed to a listener. -type InitializationData struct { - // HasEventAlignedWrites indicates that the blockchain data source will emit KV-pair events - // in an order aligned with transaction, message and event callbacks. If this is true - // then indexers can assume that KV-pair data is associated with these specific transactions, messages - // and events. This may be useful for indexers which store a log of all operations (such as immutable - // or version controlled databases) so that the history log can include fine grain correlation between - // state updates and transactions, messages and events. If this value is false, then indexers should - // assume that KV-pair data occurs out of order with respect to transaction, message and event callbacks - - // the only safe assumption being that KV-pair data is associated with the block in which it was emitted. - HasEventAlignedWrites bool -} - -type ModuleInitializationData struct { - ModuleName string - Schema schema.ModuleSchema -} - -type StartBlockData struct { - // Height is the height of the block. - Height uint64 - - // Bytes is the raw byte representation of the block header. - HeaderBytes ToBytes - - // JSON is the JSON representation of the block header. It should generally be a JSON object. - HeaderJSON ToJSON -} - -// TxData represents the raw transaction data that is passed to a listener. -type TxData struct { - // TxIndex is the index of the transaction in the block. - TxIndex int32 - - // Bytes is the raw byte representation of the transaction. - Bytes ToBytes - - // JSON is the JSON representation of the transaction. It should generally be a JSON object. - JSON ToJSON -} - -// EventData represents event data that is passed to a listener. -type EventData struct { - // TxIndex is the index of the transaction in the block to which this event is associated. - // It should be set to a negative number if the event is not associated with a transaction. - // Canonically -1 should be used to represent begin block processing and -2 should be used to - // represent end block processing. - TxIndex int32 - - // MsgIndex is the index of the message in the transaction to which this event is associated. - // If TxIndex is negative, this index could correspond to the index of the message in - // begin or end block processing if such indexes exist, or it can be set to zero. - MsgIndex uint32 - - // EventIndex is the index of the event in the message to which this event is associated. - EventIndex uint32 - - // Type is the type of the event. - Type string - - // Data is the JSON representation of the event data. It should generally be a JSON object. - Data ToJSON -} - -// ToBytes is a function that lazily returns the raw byte representation of data. -type ToBytes = func() ([]byte, error) - -// ToJSON is a function that lazily returns the JSON representation of data. -type ToJSON = func() (json.RawMessage, error) - -type KVPairData struct { - Updates []ModuleKVPairUpdate -} - -type ModuleKVPairUpdate struct { - // ModuleName is the name of the module that the key-value pair belongs to. - ModuleName string - - Update schema.KVPairUpdate -} - -// ObjectUpdateData represents object update data that is passed to a listener. -type ObjectUpdateData struct { - // ModuleName is the name of the module that the update corresponds to. - ModuleName string - - // Updates are the object updates. - Updates []schema.ObjectUpdate -} - -type CommitData struct{} diff --git a/schema/appdata/debug.go b/schema/appdata/debug.go deleted file mode 100644 index bff6a851d533..000000000000 --- a/schema/appdata/debug.go +++ /dev/null @@ -1,31 +0,0 @@ -package appdata - -import ( - "fmt" - "io" -) - -func DebugListener(out io.Writer) Listener { - res := packetForwarder(func(p Packet) error { - _, err := fmt.Fprintln(out, p) - return err - }) - //res.Initialize = func(ctx context.Context, data InitializationData) (lastBlockPersisted int64, err error) { - // _, err = fmt.Fprintf(out, "Initialize: %v\n", data) - // return 0, err - //} - return res -} - -func packetForwarder(f func(Packet) error) Listener { - return Listener{ - //Initialize: nil, // can't be forwarded - InitializeModuleData: func(data ModuleInitializationData) error { return f(data) }, - OnTx: func(data TxData) error { return f(data) }, - OnEvent: func(data EventData) error { return f(data) }, - OnKVPair: func(data KVPairData) error { return f(data) }, - OnObjectUpdate: func(data ObjectUpdateData) error { return f(data) }, - StartBlock: func(data StartBlockData) error { return f(data) }, - Commit: func(data CommitData) error { return f(data) }, - } -} diff --git a/schema/appdata/listener.go b/schema/appdata/listener.go index e1c6f1662584..e0868ae0c474 100644 --- a/schema/appdata/listener.go +++ b/schema/appdata/listener.go @@ -1,31 +1,30 @@ package appdata +import ( + "encoding/json" + + "cosmossdk.io/schema" +) + // Listener is an interface that defines methods for listening to both raw and logical blockchain data. // It is valid for any of the methods to be nil, in which case the listener will not be called for that event. // Listeners should understand the guarantees that are provided by the source they are listening to and // understand which methods will or will not be called. For instance, most blockchains will not do logical -// decoding of data out of the box, so the InitializeModuleData and OnObjectUpdate methods will not be called. +// decoding of data out of the box, so the InitializeModuleSchema and OnObjectUpdate methods will not be called. // These methods will only be called when listening logical decoding is setup. type Listener struct { - //// TODO: this is the only method that can't be packetized so muxing is a bit more complex - maybe we should pull this out and create a separate initialization mechanism - //// Initialize is called when the listener is initialized before any other methods are called. - //// The lastBlockPersisted return value should be the last block height the listener persisted if it is - //// persisting block data, 0 if it is not interested in persisting block data, or -1 if it is - //// persisting block data but has not persisted any data yet. This check allows the indexer - //// framework to ensure that the listener has not missed blocks. Data sources MUST call - //// initialize before any other method is called, otherwise, no data will be processed. - //Initialize func(context.Context, InitializationData) (lastBlockPersisted int64, err error) - - // InitializeModuleData should be called whenever the blockchain process starts OR whenever - // logical decoding of a module is initiated. An indexer listening to this event - // should ensure that they have performed whatever initialization steps (such as database - // migrations) required to receive OnObjectUpdate events for the given module. If the - // indexer's schema is incompatible with the module's on-chain schema, the listener should return - // an error. Module names must conform to the NameFormat regular expression. - InitializeModuleData func(ModuleInitializationData) error + // Initialize is called when the listener is initialized before any other methods are called. + // The lastBlockPersisted return value should be the last block height the listener persisted if it is + // persisting block data, 0 if it is not interested in persisting block data, or -1 if it is + // persisting block data but has not persisted any data yet. This check allows the indexer + // framework to ensure that the listener has not missed blocks. + Initialize func(InitializationData) (lastBlockPersisted int64, err error) // StartBlock is called at the beginning of processing a block. - StartBlock func(StartBlockData) error + StartBlock func(uint64) error + + // OnBlockHeader is called when a block header is received. + OnBlockHeader func(BlockHeaderData) error // OnTx is called when a transaction is received. OnTx func(TxData) error @@ -35,16 +34,89 @@ type Listener struct { // OnKVPair is called when a key-value has been written to the store for a given module. // Module names must conform to the NameFormat regular expression. - OnKVPair func(updates KVPairData) error + OnKVPair func(moduleName string, key, value []byte, delete bool) error + + // Commit is called when state is committed, usually at the end of a block. Any + // indexers should commit their data when this is called and return an error if + // they are unable to commit. + Commit func() error + + // InitializeModuleSchema should be called whenever the blockchain process starts OR whenever + // logical decoding of a module is initiated. An indexer listening to this event + // should ensure that they have performed whatever initialization steps (such as database + // migrations) required to receive OnObjectUpdate events for the given module. If the + // indexer's schema is incompatible with the module's on-chain schema, the listener should return + // an error. Module names must conform to the NameFormat regular expression. + InitializeModuleSchema func(moduleName string, moduleSchema schema.ModuleSchema) error // OnObjectUpdate is called whenever an object is updated in a module's state. This is only called // when logical data is available. It should be assumed that the same data in raw form // is also passed to OnKVPair. Module names must conform to the NameFormat regular expression. - OnObjectUpdate func(ObjectUpdateData) error + OnObjectUpdate func(moduleName string, update schema.ObjectUpdate) error +} - // Commit is called when state is committed, usually at the end of a block. Any - // indexers should commit their data when this is called and return an error if - // they are unable to commit. Data sources MUST call Commit when data is committed, - // otherwise it should be assumed that indexers have not persisted their state. - Commit func(CommitData) error +// InitializationData represents initialization data that is passed to a listener. +type InitializationData struct { + // HasEventAlignedWrites indicates that the blockchain data source will emit KV-pair events + // in an order aligned with transaction, message and event callbacks. If this is true + // then indexers can assume that KV-pair data is associated with these specific transactions, messages + // and events. This may be useful for indexers which store a log of all operations (such as immutable + // or version controlled databases) so that the history log can include fine grain correlation between + // state updates and transactions, messages and events. If this value is false, then indexers should + // assume that KV-pair data occurs out of order with respect to transaction, message and event callbacks - + // the only safe assumption being that KV-pair data is associated with the block in which it was emitted. + HasEventAlignedWrites bool +} + +// BlockHeaderData represents the raw block header data that is passed to a listener. +type BlockHeaderData struct { + // Height is the height of the block. + Height uint64 + + // Bytes is the raw byte representation of the block header. + Bytes ToBytes + + // JSON is the JSON representation of the block header. It should generally be a JSON object. + JSON ToJSON +} + +// TxData represents the raw transaction data that is passed to a listener. +type TxData struct { + // TxIndex is the index of the transaction in the block. + TxIndex int32 + + // Bytes is the raw byte representation of the transaction. + Bytes ToBytes + + // JSON is the JSON representation of the transaction. It should generally be a JSON object. + JSON ToJSON } + +// EventData represents event data that is passed to a listener. +type EventData struct { + // TxIndex is the index of the transaction in the block to which this event is associated. + // It should be set to a negative number if the event is not associated with a transaction. + // Canonically -1 should be used to represent begin block processing and -2 should be used to + // represent end block processing. + TxIndex int32 + + // MsgIndex is the index of the message in the transaction to which this event is associated. + // If TxIndex is negative, this index could correspond to the index of the message in + // begin or end block processing if such indexes exist, or it can be set to zero. + MsgIndex uint32 + + // EventIndex is the index of the event in the message to which this event is associated. + EventIndex uint32 + + // Type is the type of the event. + Type string + + // Data is the JSON representation of the event data. It should generally be a JSON object. + Data ToJSON +} + +// ToBytes is a function that lazily returns the raw byte representation of data. +type ToBytes = func() ([]byte, error) + +// ToJSON is a function that lazily returns the JSON representation of data. +type ToJSON = func() (json.RawMessage, error) diff --git a/schema/appdata/mux.go b/schema/appdata/mux.go deleted file mode 100644 index af2ffd9e3145..000000000000 --- a/schema/appdata/mux.go +++ /dev/null @@ -1,121 +0,0 @@ -package appdata - -// ListenerMux returns a listener that forwards received events to all the provided listeners and only -// registers a callback if a non-nil callback is present in at least one of the listeners. -func ListenerMux(listeners ...Listener) Listener { - mux := Listener{} - - for _, l := range listeners { - if l.InitializeModuleData != nil { - mux.InitializeModuleData = func(data ModuleInitializationData) error { - for _, l := range listeners { - if l.InitializeModuleData != nil { - if err := l.InitializeModuleData(data); err != nil { - return err - } - } - } - return nil - } - break - } - } - - for _, l := range listeners { - if l.StartBlock != nil { - mux.StartBlock = func(data StartBlockData) error { - for _, l := range listeners { - if l.StartBlock != nil { - if err := l.StartBlock(data); err != nil { - return err - } - } - } - return nil - } - break - } - } - - for _, l := range listeners { - if l.OnTx != nil { - mux.OnTx = func(data TxData) error { - for _, l := range listeners { - if l.OnTx != nil { - if err := l.OnTx(data); err != nil { - return err - } - } - } - return nil - } - break - } - } - - for _, listener := range listeners { - if listener.OnEvent != nil { - mux.OnEvent = func(data EventData) error { - for _, l := range listeners { - if l.OnEvent != nil { - if err := l.OnEvent(data); err != nil { - return err - } - } - } - return nil - } - break - } - } - - for _, listener := range listeners { - if listener.OnKVPair != nil { - mux.OnKVPair = func(data KVPairData) error { - for _, l := range listeners { - if l.OnKVPair != nil { - if err := l.OnKVPair(data); err != nil { - return err - } - } - } - return nil - } - break - } - } - - for _, listener := range listeners { - if listener.OnObjectUpdate != nil { - mux.OnObjectUpdate = func(data ObjectUpdateData) error { - for _, l := range listeners { - if l.OnObjectUpdate != nil { - if err := l.OnObjectUpdate(data); err != nil { - return err - } - } - } - return nil - } - break - } - } - - for _, listener := range listeners { - if listener.Commit != nil { - mux.Commit = func(data CommitData) error { - for _, l := range listeners { - if l.Commit != nil { - if err := l.Commit(data); err != nil { - return err - } - } - } - return nil - } - break - } - } - - return mux -} diff --git a/schema/appdata/packet.go b/schema/appdata/packet.go deleted file mode 100644 index 42f8d4b9972b..000000000000 --- a/schema/appdata/packet.go +++ /dev/null @@ -1,58 +0,0 @@ -package appdata - -type Packet interface { - apply(*Listener) error -} - -func (l Listener) SendPacket(p Packet) error { - return p.apply(&l) -} - -func (m ModuleInitializationData) apply(l *Listener) error { - if l.InitializeModuleData == nil { - return nil - } - return l.InitializeModuleData(m) -} - -func (b StartBlockData) apply(l *Listener) error { - if l.StartBlock == nil { - return nil - } - return l.StartBlock(b) -} - -func (t TxData) apply(l *Listener) error { - if l.OnTx == nil { - return nil - } - return l.OnTx(t) -} - -func (e EventData) apply(l *Listener) error { - if l.OnEvent == nil { - return nil - } - return l.OnEvent(e) -} - -func (k KVPairData) apply(l *Listener) error { - if l.OnKVPair == nil { - return nil - } - return l.OnKVPair(k) -} - -func (o ObjectUpdateData) apply(l *Listener) error { - if l.OnObjectUpdate == nil { - return nil - } - return l.OnObjectUpdate(o) -} - -func (c CommitData) apply(l *Listener) error { - if l.Commit == nil { - return nil - } - return l.Commit(c) -} diff --git a/schema/decoder.go b/schema/decoder.go index 86aedec9f9dc..1d228d1c06ae 100644 --- a/schema/decoder.go +++ b/schema/decoder.go @@ -18,23 +18,8 @@ type ModuleCodec struct { KVDecoder KVDecoder } -// KVDecoder is a function that decodes a key-value pair into one or more ObjectUpdate's. -// If the KV-pair doesn't represent object updates, the function should return nil as the first -// and no error. The error result should only be non-nil when the decoder expected -// to parse a valid update and was unable to. In the case of an error, the decoder may return -// a non-nil value for the first return value, which can indicate which parts of the update -// were decodable to aid debugging. -type KVDecoder = func(KVPairUpdate) ([]ObjectUpdate, error) - -// KVPairUpdate represents a key-value pair set or delete. -type KVPairUpdate struct { - // Key is the key of the key-value pair. - Key []byte - - // Value is the value of the key-value pair. It should be ignored when Delete is true. - Value []byte - - // Delete is a flag that indicates that the key-value pair was deleted. If it is false, - // then it is assumed that this has been a set operation. - Delete bool -} +// KVDecoder is a function that decodes a key-value pair into an ObjectUpdate. +// If the KV-pair doesn't represent an object update, the function should return false +// as the second return value. Error should only be non-nil when the decoder expected +// to parse a valid update and was unable to. +type KVDecoder = func(key, value []byte) (ObjectUpdate, bool, error) diff --git a/schema/enum.go b/schema/enum.go index 6e0be7c61533..0ed710af37f9 100644 --- a/schema/enum.go +++ b/schema/enum.go @@ -5,9 +5,6 @@ import "fmt" // EnumDefinition represents the definition of an enum type. type EnumDefinition struct { // Name is the name of the enum type. It must conform to the NameFormat regular expression. - // Its name must be unique between all enum types and object types in the module. - // The same enum, however, can be used in multiple object types and fields as long as the - // definition is identical each time Name string // Values is a list of distinct, non-empty values that are part of the enum type. @@ -47,31 +44,3 @@ func (e EnumDefinition) ValidateValue(value string) error { } return fmt.Errorf("value %q is not a valid enum value for %s", value, e.Name) } - -// checkEnumCompatibility checks that the enum values are consistent across object types and fields. -func checkEnumCompatibility(enumValueMap map[string]map[string]bool, field Field) error { - if field.Kind != EnumKind { - return nil - } - - enum := field.EnumDefinition - - if existing, ok := enumValueMap[enum.Name]; ok { - if len(existing) != len(enum.Values) { - return fmt.Errorf("enum %q has different number of values in different object types", enum.Name) - } - - for _, value := range enum.Values { - if !existing[value] { - return fmt.Errorf("enum %q has different values in different object types", enum.Name) - } - } - } else { - valueMap := map[string]bool{} - for _, value := range enum.Values { - valueMap[value] = true - } - enumValueMap[enum.Name] = valueMap - } - return nil -} diff --git a/schema/field.go b/schema/field.go index 6e421df10cc3..a3b6ea2784cf 100644 --- a/schema/field.go +++ b/schema/field.go @@ -10,11 +10,10 @@ type Field struct { // Kind is the basic type of the field. Kind Kind - // Nullable indicates whether null values are accepted for the field. Key fields CANNOT be nullable. + // Nullable indicates whether null values are accepted for the field. Nullable bool // AddressPrefix is the address prefix of the field's kind, currently only used for Bech32AddressKind. - // TODO: add validation for valid address prefixes AddressPrefix string // EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind. diff --git a/schema/fields.go b/schema/fields.go index be08ca66ef3d..1500d40bfa2a 100644 --- a/schema/fields.go +++ b/schema/fields.go @@ -2,15 +2,15 @@ package schema import "fmt" -// ValidateObjectKey validates that the value conforms to the set of fields as a Key in an ObjectUpdate. +// ValidateForKeyFields validates that the value conforms to the set of fields as a Key in an ObjectUpdate. // See ObjectUpdate.Key for documentation on the requirements of such keys. -func ValidateObjectKey(keyFields []Field, value interface{}) error { +func ValidateForKeyFields(keyFields []Field, value interface{}) error { return validateFieldsValue(keyFields, value) } -// ValidateObjectValue validates that the value conforms to the set of fields as a Value in an ObjectUpdate. +// ValidateForValueFields validates that the value conforms to the set of fields as a Value in an ObjectUpdate. // See ObjectUpdate.Value for documentation on the requirements of such values. -func ValidateObjectValue(valueFields []Field, value interface{}) error { +func ValidateForValueFields(valueFields []Field, value interface{}) error { valueUpdates, ok := value.(ValueUpdates) if !ok { return validateFieldsValue(valueFields, value) diff --git a/schema/fields_test.go b/schema/fields_test.go index befa968657d1..aa22e02a1466 100644 --- a/schema/fields_test.go +++ b/schema/fields_test.go @@ -56,7 +56,7 @@ func TestValidateForKeyFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateObjectKey(tt.keyFields, tt.key) + err := ValidateForKeyFields(tt.keyFields, tt.key) if tt.errContains == "" { if err != nil { t.Fatalf("unexpected error: %v", err) @@ -128,7 +128,7 @@ func TestValidateForValueFields(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateObjectValue(tt.valueFields, tt.value) + err := ValidateForValueFields(tt.valueFields, tt.value) if tt.errContains == "" { if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/schema/indexing/indexer.go b/schema/indexing/indexer.go deleted file mode 100644 index 7568df1a3f8a..000000000000 --- a/schema/indexing/indexer.go +++ /dev/null @@ -1,36 +0,0 @@ -package indexing - -import ( - "context" - "fmt" - - "cosmossdk.io/schema/appdata" - "cosmossdk.io/schema/logutil" -) - -type Indexer interface { - Initialize(context.Context, InitializationData) (InitializationResult, error) -} - -type IndexerResources struct { - Logger logutil.Logger -} - -type IndexerFactory = func(options map[string]interface{}, resources IndexerResources) (Indexer, error) - -type InitializationData struct{} - -type InitializationResult struct { - Listener appdata.Listener - LastBlockPersisted int64 -} - -func RegisterIndexer(name string, factory IndexerFactory) { - if _, ok := indexerRegistry[name]; ok { - panic(fmt.Sprintf("indexer %s already registered", name)) - } - - indexerRegistry[name] = factory -} - -var indexerRegistry = map[string]IndexerFactory{} diff --git a/schema/kind.go b/schema/kind.go index c3fefad52c8a..91602b0bd2bf 100644 --- a/schema/kind.go +++ b/schema/kind.go @@ -5,7 +5,6 @@ import ( "fmt" "regexp" "time" - "unicode/utf8" ) // Kind represents the basic type of a field in an object. @@ -17,8 +16,7 @@ const ( // InvalidKind indicates that an invalid type. InvalidKind Kind = iota - // StringKind is a string type and values of this type must be of the go type string - // containing valid UTF-8 and cannot contain null characters. + // StringKind is a string type and values of this type must be of the go type string. StringKind // BytesKind is a bytes type and values of this type must be of the go type []byte. @@ -48,14 +46,14 @@ const ( // Uint64Kind is a uint64 type and values of this type must be of the go type uint64. Uint64Kind - // IntegerStringKind represents an arbitrary precision integer number. Values of this type must + // IntegerKind represents an arbitrary precision integer number. Values of this type must // be of the go type string and formatted as base10 integers, specifically matching to // the IntegerFormat regex. - IntegerStringKind + IntegerKind - // DecimalStringKind represents an arbitrary precision decimal or integer number. Values of this type + // DecimalKind represents an arbitrary precision decimal or integer number. Values of this type // must be of the go type string and match the DecimalFormat regex. - DecimalStringKind + DecimalKind // BoolKind is a boolean type and values of this type must be of the go type bool. BoolKind @@ -136,9 +134,9 @@ func (t Kind) String() string { return "int64" case Uint64Kind: return "uint64" - case DecimalStringKind: + case DecimalKind: return "decimal" - case IntegerStringKind: + case IntegerKind: return "integer" case BoolKind: return "bool" @@ -218,13 +216,13 @@ func (t Kind) ValidateValueType(value interface{}) error { if !ok { return fmt.Errorf("expected uint64, got %T", value) } - case IntegerStringKind: + case IntegerKind: _, ok := value.(string) if !ok { return fmt.Errorf("expected string, got %T", value) } - case DecimalStringKind: + case DecimalKind: _, ok := value.(string) if !ok { return fmt.Errorf("expected string, got %T", value) @@ -285,23 +283,11 @@ func (t Kind) ValidateValue(value interface{}) error { } switch t { - case StringKind: - str := value.(string) - if !utf8.ValidString(str) { - return fmt.Errorf("expected valid utf-8 string, got %s", value) - } - - // check for null characters - for _, r := range str { - if r == 0 { - return fmt.Errorf("expected string without null characters, got %s", value) - } - } - case IntegerStringKind: + case IntegerKind: if !integerRegex.Match([]byte(value.(string))) { return fmt.Errorf("expected base10 integer, got %s", value) } - case DecimalStringKind: + case DecimalKind: if !decimalRegex.Match([]byte(value.(string))) { return fmt.Errorf("expected decimal number, got %s", value) } @@ -321,7 +307,7 @@ var ( ) // KindForGoValue finds the simplest kind that can represent the given go value. It will not, however, -// return kinds such as IntegerStringKind, DecimalStringKind, Bech32AddressKind, or EnumKind which all can be +// return kinds such as IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind which all can be // represented as strings. func KindForGoValue(value interface{}) Kind { switch value.(type) { diff --git a/schema/kind_test.go b/schema/kind_test.go index 49799fe4956c..c287bb61aa78 100644 --- a/schema/kind_test.go +++ b/schema/kind_test.go @@ -53,12 +53,12 @@ func TestKind_ValidateValueType(t *testing.T) { {kind: Int64Kind, value: int32(1), valid: false}, {kind: Uint64Kind, value: uint64(1), valid: true}, {kind: Uint64Kind, value: uint32(1), valid: false}, - {kind: IntegerStringKind, value: "1", valid: true}, - {kind: IntegerStringKind, value: int32(1), valid: false}, - {kind: DecimalStringKind, value: "1.0", valid: true}, - {kind: DecimalStringKind, value: "1", valid: true}, - {kind: DecimalStringKind, value: "1.1e4", valid: true}, - {kind: DecimalStringKind, value: int32(1), valid: false}, + {kind: IntegerKind, value: "1", valid: true}, + {kind: IntegerKind, value: int32(1), valid: false}, + {kind: DecimalKind, value: "1.0", valid: true}, + {kind: DecimalKind, value: "1", valid: true}, + {kind: DecimalKind, value: "1.1e4", valid: true}, + {kind: DecimalKind, value: int32(1), valid: false}, {kind: Bech32AddressKind, value: []byte("hello"), valid: true}, {kind: Bech32AddressKind, value: 1, valid: false}, {kind: BoolKind, value: true, valid: true}, @@ -110,59 +110,55 @@ func TestKind_ValidateValue(t *testing.T) { {Int64Kind, int64(1), true}, {Int32Kind, "abc", false}, {BytesKind, nil, false}, - // string must be valid UTF-8 - {StringKind, string([]byte{0xff, 0xfe, 0xfd}), false}, - // strings with null characters are invalid - {StringKind, string([]byte{1, 2, 0, 3}), false}, // check integer, decimal and json more thoroughly - {IntegerStringKind, "1", true}, - {IntegerStringKind, "0", true}, - {IntegerStringKind, "10", true}, - {IntegerStringKind, "-100", true}, - {IntegerStringKind, "1.0", false}, - {IntegerStringKind, "00", true}, // leading zeros are allowed - {IntegerStringKind, "001", true}, - {IntegerStringKind, "-01", true}, + {IntegerKind, "1", true}, + {IntegerKind, "0", true}, + {IntegerKind, "10", true}, + {IntegerKind, "-100", true}, + {IntegerKind, "1.0", false}, + {IntegerKind, "00", true}, // leading zeros are allowed + {IntegerKind, "001", true}, + {IntegerKind, "-01", true}, // 100 digits - {IntegerStringKind, "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", true}, + {IntegerKind, "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", true}, // more than 100 digits - {IntegerStringKind, "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", false}, - {IntegerStringKind, "", false}, - {IntegerStringKind, "abc", false}, - {IntegerStringKind, "abc100", false}, - {DecimalStringKind, "1.0", true}, - {DecimalStringKind, "0.0", true}, - {DecimalStringKind, "-100.075", true}, - {DecimalStringKind, "1002346.000", true}, - {DecimalStringKind, "0", true}, - {DecimalStringKind, "10", true}, - {DecimalStringKind, "-100", true}, - {DecimalStringKind, "1", true}, - {DecimalStringKind, "1.0e4", true}, - {DecimalStringKind, "1.0e-4", true}, - {DecimalStringKind, "1.0e+4", true}, - {DecimalStringKind, "1.0e", false}, - {DecimalStringKind, "1.0e4.0", false}, - {DecimalStringKind, "1.0e-4.0", false}, - {DecimalStringKind, "1.0e+4.0", false}, - {DecimalStringKind, "-1.0e-4", true}, - {DecimalStringKind, "-1.0e+4", true}, - {DecimalStringKind, "-1.0E4", true}, - {DecimalStringKind, "1E-9", true}, - {DecimalStringKind, "1E-99", true}, - {DecimalStringKind, "1E+9", true}, - {DecimalStringKind, "1E+99", true}, + {IntegerKind, "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", false}, + {IntegerKind, "", false}, + {IntegerKind, "abc", false}, + {IntegerKind, "abc100", false}, + {DecimalKind, "1.0", true}, + {DecimalKind, "0.0", true}, + {DecimalKind, "-100.075", true}, + {DecimalKind, "1002346.000", true}, + {DecimalKind, "0", true}, + {DecimalKind, "10", true}, + {DecimalKind, "-100", true}, + {DecimalKind, "1", true}, + {DecimalKind, "1.0e4", true}, + {DecimalKind, "1.0e-4", true}, + {DecimalKind, "1.0e+4", true}, + {DecimalKind, "1.0e", false}, + {DecimalKind, "1.0e4.0", false}, + {DecimalKind, "1.0e-4.0", false}, + {DecimalKind, "1.0e+4.0", false}, + {DecimalKind, "-1.0e-4", true}, + {DecimalKind, "-1.0e+4", true}, + {DecimalKind, "-1.0E4", true}, + {DecimalKind, "1E-9", true}, + {DecimalKind, "1E-99", true}, + {DecimalKind, "1E+9", true}, + {DecimalKind, "1E+99", true}, // 50 digits before and after the decimal point - {DecimalStringKind, "10000000000000000000000000000000000000000000000000.10000000000000000000000000000000000000000000000001", true}, + {DecimalKind, "10000000000000000000000000000000000000000000000000.10000000000000000000000000000000000000000000000001", true}, // too many digits before the decimal point - {DecimalStringKind, "10000000000000000000000000000000000000000000000000000000000000000000000000", false}, + {DecimalKind, "10000000000000000000000000000000000000000000000000000000000000000000000000", false}, // too many digits after the decimal point - {DecimalStringKind, "1.0000000000000000000000000000000000000000000000000000000000000000000000001", false}, + {DecimalKind, "1.0000000000000000000000000000000000000000000000000000000000000000000000001", false}, // exponent too big - {DecimalStringKind, "1E-999", false}, - {DecimalStringKind, "", false}, - {DecimalStringKind, "abc", false}, - {DecimalStringKind, "abc", false}, + {DecimalKind, "1E-999", false}, + {DecimalKind, "", false}, + {DecimalKind, "abc", false}, + {DecimalKind, "abc", false}, {JSONKind, json.RawMessage(`{"a":10}`), true}, {JSONKind, json.RawMessage("10"), true}, {JSONKind, json.RawMessage("10.0"), true}, @@ -204,8 +200,8 @@ func TestKind_String(t *testing.T) { {Uint32Kind, "uint32"}, {Int64Kind, "int64"}, {Uint64Kind, "uint64"}, - {IntegerStringKind, "integer"}, - {DecimalStringKind, "decimal"}, + {IntegerKind, "integer"}, + {DecimalKind, "decimal"}, {BoolKind, "bool"}, {TimeKind, "time"}, {DurationKind, "duration"}, diff --git a/schema/module_schema.go b/schema/module_schema.go index 9412c4456cbe..170288b65023 100644 --- a/schema/module_schema.go +++ b/schema/module_schema.go @@ -10,13 +10,56 @@ type ModuleSchema struct { // Validate validates the module schema. func (s ModuleSchema) Validate() error { - enumValueMap := map[string]map[string]bool{} for _, objType := range s.ObjectTypes { - if err := objType.validate(enumValueMap); err != nil { + if err := objType.Validate(); err != nil { return err } } + // validate that shared enum types are consistent across object types + enumValueMap := map[string]map[string]bool{} + for _, objType := range s.ObjectTypes { + for _, field := range objType.KeyFields { + err := checkEnum(enumValueMap, field) + if err != nil { + return err + } + } + for _, field := range objType.ValueFields { + err := checkEnum(enumValueMap, field) + if err != nil { + return err + } + } + } + + return nil +} + +func checkEnum(enumValueMap map[string]map[string]bool, field Field) error { + if field.Kind != EnumKind { + return nil + } + + enum := field.EnumDefinition + + if existing, ok := enumValueMap[enum.Name]; ok { + if len(existing) != len(enum.Values) { + return fmt.Errorf("enum %q has different number of values in different object types", enum.Name) + } + + for _, value := range enum.Values { + if !existing[value] { + return fmt.Errorf("enum %q has different values in different object types", enum.Name) + } + } + } else { + valueMap := map[string]bool{} + for _, value := range enum.Values { + valueMap[value] = true + } + enumValueMap[enum.Name] = valueMap + } return nil } diff --git a/schema/module_schema_test.go b/schema/module_schema_test.go index d04327811be6..d2b6220a4f2c 100644 --- a/schema/module_schema_test.go +++ b/schema/module_schema_test.go @@ -110,39 +110,6 @@ func TestModuleSchema_Validate(t *testing.T) { }, errContains: "different values", }, - { - name: "same enum", - moduleSchema: ModuleSchema{ - ObjectTypes: []ObjectType{ - { - Name: "object1", - KeyFields: []Field{ - { - Name: "k", - Kind: EnumKind, - EnumDefinition: EnumDefinition{ - Name: "enum1", - Values: []string{"a", "b"}, - }, - }, - }, - }, - { - Name: "object2", - KeyFields: []Field{ - { - Name: "k", - Kind: EnumKind, - EnumDefinition: EnumDefinition{ - Name: "enum1", - Values: []string{"a", "b"}, - }, - }, - }, - }, - }, - }, - }, } for _, tt := range tests { diff --git a/schema/object_type.go b/schema/object_type.go index 9560c5d4e35f..f991329eff0a 100644 --- a/schema/object_type.go +++ b/schema/object_type.go @@ -11,7 +11,7 @@ type ObjectType struct { // KeyFields is a list of fields that make up the primary key of the object. // It can be empty in which case indexers should assume that this object is // a singleton and only has one value. Field names must be unique within the - // object between both key and value fields. Key fields CANNOT be nullable. + // object between both key and value fields. KeyFields []Field // ValueFields is a list of fields that are not part of the primary key of the object. @@ -29,35 +29,20 @@ type ObjectType struct { // Validate validates the object type. func (o ObjectType) Validate() error { - return o.validate(map[string]map[string]bool{}) -} - -// validate validates the object type with an enumValueMap that can be -// shared across a whole module schema. -func (o ObjectType) validate(enumValueMap map[string]map[string]bool) error { if !ValidateName(o.Name) { return fmt.Errorf("invalid object type name %q", o.Name) } fieldNames := map[string]bool{} - for _, field := range o.KeyFields { if err := field.Validate(); err != nil { return fmt.Errorf("invalid key field %q: %w", field.Name, err) } - if field.Nullable { - return fmt.Errorf("key field %q cannot be nullable", field.Name) - } - if fieldNames[field.Name] { return fmt.Errorf("duplicate field name %q", field.Name) } fieldNames[field.Name] = true - - if err := checkEnumCompatibility(enumValueMap, field); err != nil { - return err - } } for _, field := range o.ValueFields { @@ -69,10 +54,6 @@ func (o ObjectType) validate(enumValueMap map[string]map[string]bool) error { return fmt.Errorf("duplicate field name %q", field.Name) } fieldNames[field.Name] = true - - if err := checkEnumCompatibility(enumValueMap, field); err != nil { - return err - } } if len(o.KeyFields) == 0 && len(o.ValueFields) == 0 { @@ -88,7 +69,7 @@ func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error { return fmt.Errorf("object type name %q does not match update type name %q", o.Name, update.TypeName) } - if err := ValidateObjectKey(o.KeyFields, update.Key); err != nil { + if err := ValidateForKeyFields(o.KeyFields, update.Key); err != nil { return fmt.Errorf("invalid key for object type %q: %w", update.TypeName, err) } @@ -96,5 +77,5 @@ func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error { return nil } - return ValidateObjectValue(o.ValueFields, update.Value) + return ValidateForValueFields(o.ValueFields, update.Value) } diff --git a/schema/object_type_test.go b/schema/object_type_test.go index 0a78a371aa96..32e8885bbce2 100644 --- a/schema/object_type_test.go +++ b/schema/object_type_test.go @@ -148,47 +148,6 @@ func TestObjectType_Validate(t *testing.T) { }, errContains: "duplicate field name", }, - { - name: "nullable key field", - objectType: ObjectType{ - Name: "objectNullKey", - KeyFields: []Field{ - { - Name: "field1", - Kind: StringKind, - Nullable: true, - }, - }, - }, - errContains: "key field \"field1\" cannot be nullable", - }, - { - name: "duplicate incompatible enum", - objectType: ObjectType{ - Name: "objectWithEnums", - KeyFields: []Field{ - { - Name: "key", - Kind: EnumKind, - EnumDefinition: EnumDefinition{ - Name: "enum1", - Values: []string{"a", "b"}, - }, - }, - }, - ValueFields: []Field{ - { - Name: "value", - Kind: EnumKind, - EnumDefinition: EnumDefinition{ - Name: "enum1", - Values: []string{"c", "b"}, - }, - }, - }, - }, - errContains: "enum \"enum1\" has different values", - }, } for _, tt := range tests { diff --git a/schema/testing/CHANGELOG.md b/schema/testing/CHANGELOG.md deleted file mode 100644 index 0c3c9d03857f..000000000000 --- a/schema/testing/CHANGELOG.md +++ /dev/null @@ -1,37 +0,0 @@ - - -# Changelog - -## [Unreleased] diff --git a/schema/testing/README.md b/schema/testing/README.md deleted file mode 100644 index 36f73a566c83..000000000000 --- a/schema/testing/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Schema Testing - -This module contains core test utilities and fixtures for testing schema and indexer functionality. It is managed as a separate go module to manage versions better and allow for dependencies on useful testing libraries without imposing those elsewhere. \ No newline at end of file diff --git a/schema/testing/app.go b/schema/testing/app.go deleted file mode 100644 index feda6d036346..000000000000 --- a/schema/testing/app.go +++ /dev/null @@ -1,21 +0,0 @@ -package schematesting - -import ( - "fmt" - - "pgregory.net/rapid" - - "cosmossdk.io/schema" -) - -// AppSchemaGen generates random valid app schemas, essentially a map of module names to module schemas. -var AppSchemaGen = rapid.Custom(func(t *rapid.T) map[string]schema.ModuleSchema { - schema := make(map[string]schema.ModuleSchema) - numModules := rapid.IntRange(1, 10).Draw(t, "numModules") - for i := 0; i < numModules; i++ { - moduleName := NameGen.Draw(t, "moduleName") - moduleSchema := ModuleSchemaGen.Draw(t, fmt.Sprintf("moduleSchema[%s]", moduleName)) - schema[moduleName] = moduleSchema - } - return schema -}) diff --git a/schema/testing/app_test.go b/schema/testing/app_test.go deleted file mode 100644 index ac2600e238f2..000000000000 --- a/schema/testing/app_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package schematesting - -import ( - "testing" - - "github.com/stretchr/testify/require" - "pgregory.net/rapid" -) - -func TestAppSchema(t *testing.T) { - rapid.Check(t, func(t *rapid.T) { - schema := AppSchemaGen.Draw(t, "schema") - for moduleName, moduleSchema := range schema { - require.NotEmpty(t, moduleName) - require.NoError(t, moduleSchema.Validate()) - } - }) -} diff --git a/schema/testing/appdatasim/app_data.go b/schema/testing/appdatasim/app_data.go deleted file mode 100644 index cf80226bfcb0..000000000000 --- a/schema/testing/appdatasim/app_data.go +++ /dev/null @@ -1,153 +0,0 @@ -package appdatasim - -import ( - "fmt" - - "pgregory.net/rapid" - - "cosmossdk.io/schema" - "cosmossdk.io/schema/appdata" - schematesting "cosmossdk.io/schema/testing" - "cosmossdk.io/schema/testing/statesim" -) - -// Options are the options for creating an app data Simulator. -type Options struct { - AppSchema map[string]schema.ModuleSchema - Listener appdata.Listener - EventAlignedWrites bool - StateSimOptions statesim.Options - StartBlockDataGen *rapid.Generator[appdata.StartBlockData] - TxDataGen *rapid.Generator[appdata.TxData] - EventDataGen *rapid.Generator[appdata.EventData] -} - -// Simulator simulates a stream of app data. -type Simulator struct { - state *statesim.App - options Options - blockNum uint64 - blockDataGen *rapid.Generator[BlockData] -} - -// BlockData represents the app data packets in a block. -type BlockData = []appdata.Packet - -// NewSimulator creates a new app data simulator with the given options. -func NewSimulator(options Options) *Simulator { - if options.AppSchema == nil { - options.AppSchema = schematesting.ExampleAppSchema - } - - sim := &Simulator{ - state: statesim.NewApp(options.AppSchema, options.StateSimOptions), - options: options, - } - - return sim -} - -// Initialize runs the initialization methods of the app data stream. -func (a *Simulator) Initialize() error { - if f := a.options.Listener.InitializeModuleData; f != nil { - err := a.state.ScanModules(func(moduleName string, mod *statesim.Module) error { - return f(appdata.ModuleInitializationData{ModuleName: moduleName, Schema: mod.ModuleSchema()}) - }) - if err != nil { - return err - } - } - - return nil -} - -// BlockDataGen generates random block data. It is expected that generated data is passed to ProcessBlockData -// to simulate the app data stream and advance app state based on the object updates in the block. The first -// packet in the block data will be a StartBlockData packet with the height set to the next block height. -func (a *Simulator) BlockDataGen() *rapid.Generator[BlockData] { - return a.BlockDataGenN(100) -} - -// BlockDataGenN creates a block data generator which allows specifying the maximum number of updates per block. -func (a *Simulator) BlockDataGenN(maxUpdatesPerBlock int) *rapid.Generator[BlockData] { - numUpdatesGen := rapid.IntRange(1, maxUpdatesPerBlock) - - return rapid.Custom(func(t *rapid.T) BlockData { - var packets BlockData - - packets = append(packets, appdata.StartBlockData{Height: a.blockNum + 1}) - - updateSet := map[string]bool{} - // filter out any updates to the same key from this block, otherwise we can end up with weird errors - updateGen := a.state.UpdateGen().Filter(func(data appdata.ObjectUpdateData) bool { - for _, update := range data.Updates { - _, existing := updateSet[fmt.Sprintf("%s:%v", data.ModuleName, update.Key)] - if existing { - return false - } - } - return true - }) - numUpdates := numUpdatesGen.Draw(t, "numUpdates") - for i := 0; i < numUpdates; i++ { - data := updateGen.Draw(t, fmt.Sprintf("update[%d]", i)) - for _, update := range data.Updates { - updateSet[fmt.Sprintf("%s:%v", data.ModuleName, update.Key)] = true - } - packets = append(packets, data) - } - - packets = append(packets, appdata.CommitData{}) - - return packets - }) -} - -// ProcessBlockData processes the given block data, advancing the app state based on the object updates in the block -// and forwarding all packets to the attached listener. It is expected that the data passed came from BlockDataGen, -// however, other data can be passed as long as the first packet is a StartBlockData packet with the block height -// set to the current block height + 1 and the last packet is a CommitData packet. -func (a *Simulator) ProcessBlockData(data BlockData) error { - if len(data) < 2 { - return fmt.Errorf("block data must contain at least two packets") - } - - if startBlock, ok := data[0].(appdata.StartBlockData); !ok || startBlock.Height != a.blockNum+1 { - return fmt.Errorf("first packet in block data must be a StartBlockData packet with height %d", a.blockNum+1) - } - - if _, ok := data[len(data)-1].(appdata.CommitData); !ok { - return fmt.Errorf("last packet in block data must be a CommitData packet") - } - - // advance the block height - a.blockNum++ - - for _, packet := range data { - // apply state updates from object updates - if updateData, ok := packet.(appdata.ObjectUpdateData); ok { - err := a.state.ApplyUpdate(updateData) - if err != nil { - return err - } - } - - // send the packet to the listener - err := a.options.Listener.SendPacket(packet) - if err != nil { - return err - } - } - - return nil -} - -// AppState returns the current app state backing the simulator. -func (a *Simulator) AppState() *statesim.App { - return a.state -} - -// BlockNum returns the current block number of the simulator. -func (a *Simulator) BlockNum() uint64 { - return a.blockNum -} diff --git a/schema/testing/appdatasim/app_data_test.go b/schema/testing/appdatasim/app_data_test.go deleted file mode 100644 index 61bd212efccc..000000000000 --- a/schema/testing/appdatasim/app_data_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package appdatasim - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/require" - "gotest.tools/v3/golden" - - "cosmossdk.io/schema/testing" -) - -func TestAppSimulator_ExampleSchema(t *testing.T) { - out := &bytes.Buffer{} - appSim := NewSimulator(Options{ - AppSchema: schematesting.ExampleAppSchema, - Listener: WriterListener(out), - }) - - require.NoError(t, appSim.Initialize()) - - blockDataGen := appSim.BlockDataGen() - - for i := 0; i < 10; i++ { - data := blockDataGen.Example(i + 1) - require.NoError(t, appSim.ProcessBlockData(data)) - } - - golden.Assert(t, out.String(), "app_sim_example_schema.txt") -} diff --git a/schema/testing/appdatasim/doc.go b/schema/testing/appdatasim/doc.go deleted file mode 100644 index 7f3493e2524b..000000000000 --- a/schema/testing/appdatasim/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package appdatasim contains utilities for simulating valid streams of app data for testing indexer implementations. -package appdatasim diff --git a/schema/testing/appdatasim/testdata/app_sim_example_schema.txt b/schema/testing/appdatasim/testdata/app_sim_example_schema.txt deleted file mode 100644 index 95281780f917..000000000000 --- a/schema/testing/appdatasim/testdata/app_sim_example_schema.txt +++ /dev/null @@ -1,91 +0,0 @@ -InitializeModuleSchema: all_kinds {"ObjectTypes":[{"Name":"test_string","KeyFields":[{"Name":"keyNotNull","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":1,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":1,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_bytes","KeyFields":[{"Name":"keyNotNull","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":2,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":2,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_int8","KeyFields":[{"Name":"keyNotNull","Kind":3,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":3,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":3,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":3,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_uint8","KeyFields":[{"Name":"keyNotNull","Kind":4,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":4,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":4,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":4,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_int16","KeyFields":[{"Name":"keyNotNull","Kind":5,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":5,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":5,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":5,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_uint16","KeyFields":[{"Name":"keyNotNull","Kind":6,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":6,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":6,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":6,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_int32","KeyFields":[{"Name":"keyNotNull","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":7,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":7,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_uint32","KeyFields":[{"Name":"keyNotNull","Kind":8,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":8,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":8,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":8,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_int64","KeyFields":[{"Name":"keyNotNull","Kind":9,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":9,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":9,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":9,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_uint64","KeyFields":[{"Name":"keyNotNull","Kind":10,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":10,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":10,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":10,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_integer","KeyFields":[{"Name":"keyNotNull","Kind":11,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":11,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":11,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":11,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_decimal","KeyFields":[{"Name":"keyNotNull","Kind":12,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":12,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":12,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":12,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_bool","KeyFields":[{"Name":"keyNotNull","Kind":13,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":13,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":13,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":13,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_time","KeyFields":[{"Name":"keyNotNull","Kind":14,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":14,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":14,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":14,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_duration","KeyFields":[{"Name":"keyNotNull","Kind":15,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":15,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":15,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":15,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_float32","KeyFields":[{"Name":"keyNotNull","Kind":16,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":16,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":16,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":16,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_float64","KeyFields":[{"Name":"keyNotNull","Kind":17,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":17,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":17,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":17,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_bech32address","KeyFields":[{"Name":"keyNotNull","Kind":18,"Nullable":false,"AddressPrefix":"cosmos","EnumDefinition":{"Name":"","Values":null}},{"Name":"keyNullable","Kind":18,"Nullable":true,"AddressPrefix":"cosmos","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"valNotNull","Kind":18,"Nullable":false,"AddressPrefix":"cosmos","EnumDefinition":{"Name":"","Values":null}},{"Name":"valNullable","Kind":18,"Nullable":true,"AddressPrefix":"cosmos","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"test_enum","KeyFields":[{"Name":"keyNotNull","Kind":19,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"test_enum","Values":["foo","bar","baz"]}},{"Name":"keyNullable","Kind":19,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"test_enum","Values":["foo","bar","baz"]}}],"ValueFields":[{"Name":"valNotNull","Kind":19,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"test_enum","Values":["foo","bar","baz"]}},{"Name":"valNullable","Kind":19,"Nullable":true,"AddressPrefix":"","EnumDefinition":{"Name":"test_enum","Values":["foo","bar","baz"]}}],"RetainDeletions":false}]} -InitializeModuleSchema: test_cases {"ObjectTypes":[{"Name":"Singleton","KeyFields":[],"ValueFields":[{"Name":"Value","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value2","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"Simple","KeyFields":[{"Name":"Key","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"Value1","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value2","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"Two Keys","KeyFields":[{"Name":"Key1","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Key2","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":null,"RetainDeletions":false},{"Name":"Three Keys","KeyFields":[{"Name":"Key1","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Key2","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Key3","Kind":10,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"Value1","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"Many Values","KeyFields":[{"Name":"Key","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"Value1","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value2","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value3","Kind":17,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value4","Kind":10,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":false},{"Name":"RetainDeletions","KeyFields":[{"Name":"Key","Kind":1,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"ValueFields":[{"Name":"Value1","Kind":7,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}},{"Name":"Value2","Kind":2,"Nullable":false,"AddressPrefix":"","EnumDefinition":{"Name":"","Values":null}}],"RetainDeletions":true}]} -Initialize: {false} -StartBlock: 1 -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"a⃝?\rA�֍","Value":{"Value1":-2147483648,"Value2":"5j6jAR12ASgIHQMBA54DACYFGgI=","Value3":1817374971955183.2},"Delete":false} -Commit -StartBlock: 2 -OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["APo3CwC1wwTmagHIAorBdQoBAQMA0woAITL/GQH6tkT/AQE=",null],"Value":["/Mt59F4hjgGqAQ0B/wWlAqn2UqEAAQIAAU8MAYoCDzs3AgJoAAE/FgNLwAnnDcIeAAUYNS1Ytg==","ActiKPrSHCMDEjMMlg04AFflAz0qxAaWAASFAwkAywIGBxQAzF8xJQ=="],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["HDUI+gAHAhpSCu4LYgAB1wMAtQAEBctXUQ==",null],"Value":{"valNullable":"BwERCwQtBAMI8Br2Awafjke1kAEDAQMA//ssKgTpMgYMAAMBB+oUGAACBBoALwcePkwA//8pHQCtTvylLIXt8A=="},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"a⃝?\rA�֍","Value":{"Value1":197503,"Value4":4093396},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_duration","Key":[-322686,-3],"Value":[4067136,null],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_int8","Key":[6,0],"Value":{"valNotNull":-59},"Delete":false} -Commit -StartBlock: 3 -OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[true,null],"Value":{},"Delete":false} -Commit -StartBlock: 4 -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"a⃝?\rA�֍","Value":{"Value1":-1,"Value2":"3CsGAls=","Value4":58783},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_string","Key":["A^'","*?{a"],"Value":{"valNullable":"‮⋁"},"Delete":false} -Commit -StartBlock: 5 -OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["APo3CwC1wwTmagHIAorBdQoBAQMA0woAITL/GQH6tkT/AQE=",null],"Value":{"valNotNull":"OgOQG+lGAgF5Y+EAAwAAAkQHATYDYXUB","valNullable":"AQGXARQPGgE7AgHIDYoAACEBC/6MyDYLuADecgDPACvqAAELpGkACwAAqOIJAgEDKw=="},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"","Value":[-8564477,"AcQ="],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_duration","Key":[-1,null],"Value":{},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"\t𓐸\u0026A","Value":[-11084,"",5.3864324145915995e+81,285497682],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"\u0026A","Value":{"Value2":"Ab/p"},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":{},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Two Keys","Key":["؁〩a_𞥟",-120667635],"Value":null,"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[false,null],"Value":[true,null],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_int8","Key":[-4,6],"Value":{},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Singleton","Key":null,"Value":{},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"\u0026`aᾼa\u0000#ᛮ𞥞aᾮ\u0026₰?𐧀~ ा၉𩮘A` ","Value":{"Value1":163,"Value2":"iwGmHAAWaAIPz6U="},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"𝞒𐄍#?ᾫ","Value":{"Value1":246341895},"Delete":false} -Commit -StartBlock: 6 -OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"\u0026`aᾼa\u0000#ᛮ𞥞aᾮ\u0026₰?𐧀~ ा၉𩮘A` ","Value":null,"Delete":true} -OnObjectUpdate: all_kinds: {"TypeName":"test_enum","Key":["bar","foo"],"Value":["bar",null],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_enum","Key":["foo","bar"],"Value":{},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bytes","Key":["BAAZ/Mo=","C7U="],"Value":{},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"Ƶa\u0026[.\u0026a.","Value":[-314,"AA0LT19MujQyf/8FbQMDawAM"],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_enum","Key":["bar","foo"],"Value":{"valNullable":"foo"},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[true,null],"Value":{"valNotNull":true,"valNullable":null},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"_$A\r","Value":{"Value1":1319133},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"","Value":[-14,"BA=="],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"Ƶa\u0026[.\u0026a.","Value":null,"Delete":true} -OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"g$🄛\u0026#?","Value":{"Value1":2,"Value2":"BvIE"},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Singleton","Key":null,"Value":null,"Delete":true} -Commit -StartBlock: 7 -OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"","Value":[-4,"nAABEGi4pg=="],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"aᱻꞘ","Value":[-11,"CA=="],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":{"Value1":-61,"Value4":4},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"+","Value":[-1891215,"+UwDKRIC"],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["NAHCtAMbAQEHTAI3DgcfG8hLASMq4DICAj41AAubARsBATQg3gCppQMAAQwHASEDAwD+55ELSKjFAC4oGwgKBA==","nyYBB4AGAJnxe792TCHxAg5qHS4A"],"Value":["AG2pEQEGAQFzIAFWAQEACv8CAgECgP8AtA==",null],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Two Keys","Key":["!฿*$\u0026AȺ#˼%\u0000ⅷ_ŕ,A",-467],"Value":null,"Delete":false} -Commit -StartBlock: 8 -OnObjectUpdate: all_kinds: {"TypeName":"test_float32","Key":[-11851129000000000000,-2014.5674],"Value":[-1.0672615e-18,2.771192],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"\u0026A","Value":[3901734,"AwMF"],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"Aः𒑨Dz؅","Value":[0,""],"Delete":false} -Commit -StartBlock: 9 -OnObjectUpdate: all_kinds: {"TypeName":"test_int64","Key":[-4,5],"Value":[100,null],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bytes","Key":["/w==",null],"Value":["IgACFRoAfgMDBMi/",null],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[true,null],"Value":null,"Delete":true} -OnObjectUpdate: test_cases: {"TypeName":"RetainDeletions","Key":"","Value":{"Value2":"BgEHAAUG0QgDAw=="},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["HDUI+gAHAhpSCu4LYgAB1wMAtQAEBctXUQ==",null],"Value":null,"Delete":true} -OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[false,null],"Value":[true,null],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bytes","Key":["/w==",null],"Value":["NQE=",null],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"\t𓐸\u0026A","Value":null,"Delete":true} -OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[false,true],"Value":[false,null],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":{"Value3":5984325148376916},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_uint16","Key":[0,null],"Value":[43,0],"Delete":false} -Commit -StartBlock: 10 -OnObjectUpdate: test_cases: {"TypeName":"Simple","Key":"\\༸՚@","Value":[6,"AA=="],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_decimal","Key":["31940815527640952","2665097019"],"Value":["930126","5301e322"],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_int16","Key":[-8610,null],"Value":[1195,-25804],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[true,null],"Value":[false,true],"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":[-1,"AwFIAwmRCA==",5.463114807757741e-9,2],"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_bool","Key":[false,false],"Value":{"valNotNull":true},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_decimal","Key":["82509790016910",null],"Value":{"valNotNull":"-2","valNullable":null},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Singleton","Key":null,"Value":{},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_int64","Key":[-4,5],"Value":null,"Delete":true} -OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["NAHCtAMbAQEHTAI3DgcfG8hLASMq4DICAj41AAubARsBATQg3gCppQMAAQwHASEDAwD+55ELSKjFAC4oGwgKBA==","nyYBB4AGAJnxe792TCHxAg5qHS4A"],"Value":null,"Delete":true} -OnObjectUpdate: all_kinds: {"TypeName":"test_bech32address","Key":["ZPpWX+smAAJmAKAc//8KTwAuXQAUHKICBwEBFFB6Bx8IVh0kugBLAVSVFhYDtDULkwIwAYwA+gfMA6k=",null],"Value":{},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Many Values","Key":"","Value":{"Value1":847967},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_decimal","Key":["50.676",null],"Value":{"valNotNull":"-11515688332E35"},"Delete":false} -OnObjectUpdate: all_kinds: {"TypeName":"test_enum","Key":["bar","foo"],"Value":{"valNotNull":"bar"},"Delete":false} -OnObjectUpdate: test_cases: {"TypeName":"Two Keys","Key":["!฿*$\u0026AȺ#˼%\u0000ⅷ_ŕ,A",-467],"Value":null,"Delete":false} -Commit diff --git a/schema/testing/appdatasim/write_listener.go b/schema/testing/appdatasim/write_listener.go deleted file mode 100644 index a176612bd9a8..000000000000 --- a/schema/testing/appdatasim/write_listener.go +++ /dev/null @@ -1,43 +0,0 @@ -package appdatasim - -import ( - "encoding/json" - "fmt" - "io" - - "cosmossdk.io/schema/appdata" -) - -// WriterListener returns an appdata.Listener that writes events to the given io.Writer deterministically -// for testing purposes. -func WriterListener(w io.Writer) appdata.Listener { - return appdata.Listener{ - StartBlock: func(data appdata.StartBlockData) error { - _, err := fmt.Fprintf(w, "StartBlock: %v\n", data) - return err - }, - OnTx: nil, - OnEvent: nil, - OnKVPair: nil, - Commit: func(data appdata.CommitData) error { - _, err := fmt.Fprintf(w, "Commit: %v\n", data) - return err - }, - InitializeModuleData: func(data appdata.ModuleInitializationData) error { - bz, err := json.Marshal(data) - if err != nil { - return err - } - _, err = fmt.Fprintf(w, "InitializeModuleData: %s\n", bz) - return err - }, - OnObjectUpdate: func(data appdata.ObjectUpdateData) error { - bz, err := json.Marshal(data) - if err != nil { - return err - } - _, err = fmt.Fprintf(w, "OnObjectUpdate: %s\n", bz) - return err - }, - } -} diff --git a/schema/testing/doc.go b/schema/testing/doc.go deleted file mode 100644 index 05f3dcb1b496..000000000000 --- a/schema/testing/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package schematesting includes property-based testing generators for creating random valid data -// for testing schemas and state representing those schemas. -package schematesting diff --git a/schema/testing/enum.go b/schema/testing/enum.go deleted file mode 100644 index cd244929c0b0..000000000000 --- a/schema/testing/enum.go +++ /dev/null @@ -1,19 +0,0 @@ -package schematesting - -import ( - "pgregory.net/rapid" - - "cosmossdk.io/schema" -) - -var enumValuesGen = rapid.SliceOfNDistinct(NameGen, 1, 10, func(x string) string { return x }) - -// EnumDefinitionGen generates random valid EnumDefinitions. -var EnumDefinitionGen = rapid.Custom(func(t *rapid.T) schema.EnumDefinition { - enum := schema.EnumDefinition{ - Name: NameGen.Draw(t, "name"), - Values: enumValuesGen.Draw(t, "values"), - } - - return enum -}) diff --git a/schema/testing/enum_test.go b/schema/testing/enum_test.go deleted file mode 100644 index 10d87db9e65d..000000000000 --- a/schema/testing/enum_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package schematesting - -import ( - "testing" - - "github.com/stretchr/testify/require" - "pgregory.net/rapid" -) - -func TestEnumDefinition(t *testing.T) { - rapid.Check(t, func(t *rapid.T) { - enumDefinition := EnumDefinitionGen.Draw(t, "enum") - require.NoError(t, enumDefinition.Validate()) - }) -} diff --git a/schema/testing/example_schema.go b/schema/testing/example_schema.go deleted file mode 100644 index df2773ea9282..000000000000 --- a/schema/testing/example_schema.go +++ /dev/null @@ -1,176 +0,0 @@ -package schematesting - -import ( - "fmt" - - "cosmossdk.io/schema" -) - -var ExampleAppSchema = map[string]schema.ModuleSchema{ - "all_kinds": mkAllKindsModule(), - "test_cases": { - ObjectTypes: []schema.ObjectType{ - { - Name: "Singleton", - KeyFields: []schema.Field{}, - ValueFields: []schema.Field{ - { - Name: "Value", - Kind: schema.StringKind, - }, - { - Name: "Value2", - Kind: schema.BytesKind, - }, - }, - }, - { - Name: "Simple", - KeyFields: []schema.Field{ - { - Name: "Key", - Kind: schema.StringKind, - }, - }, - ValueFields: []schema.Field{ - { - Name: "Value1", - Kind: schema.Int32Kind, - }, - { - Name: "Value2", - Kind: schema.BytesKind, - }, - }, - }, - { - Name: "TwoKeys", - KeyFields: []schema.Field{ - { - Name: "Key1", - Kind: schema.StringKind, - }, - { - Name: "Key2", - Kind: schema.Int32Kind, - }, - }, - }, - { - Name: "ThreeKeys", - KeyFields: []schema.Field{ - { - Name: "Key1", - Kind: schema.StringKind, - }, - { - Name: "Key2", - Kind: schema.Int32Kind, - }, - { - Name: "Key3", - Kind: schema.Uint64Kind, - }, - }, - ValueFields: []schema.Field{ - { - Name: "Value1", - Kind: schema.Int32Kind, - }, - }, - }, - { - Name: "ManyValues", - KeyFields: []schema.Field{ - { - Name: "Key", - Kind: schema.StringKind, - }, - }, - ValueFields: []schema.Field{ - { - Name: "Value1", - Kind: schema.Int32Kind, - }, - { - Name: "Value2", - Kind: schema.BytesKind, - }, - { - Name: "Value3", - Kind: schema.Float64Kind, - }, - { - Name: "Value4", - Kind: schema.Uint64Kind, - }, - }, - }, - { - Name: "RetainDeletions", - KeyFields: []schema.Field{ - { - Name: "Key", - Kind: schema.StringKind, - }, - }, - ValueFields: []schema.Field{ - { - Name: "Value1", - Kind: schema.Int32Kind, - }, - { - Name: "Value2", - Kind: schema.BytesKind, - }, - }, - RetainDeletions: true, - }, - }, - }, -} - -func mkAllKindsModule() schema.ModuleSchema { - mod := schema.ModuleSchema{} - - for i := 1; i < int(schema.MAX_VALID_KIND); i++ { - kind := schema.Kind(i) - typ := mkTestObjectType(kind) - mod.ObjectTypes = append(mod.ObjectTypes, typ) - } - - return mod -} - -func mkTestObjectType(kind schema.Kind) schema.ObjectType { - field := schema.Field{ - Kind: kind, - } - - if kind == schema.EnumKind { - field.EnumDefinition = testEnum - } - - if kind == schema.Bech32AddressKind { - field.AddressPrefix = "cosmos" - } - - keyField := field - keyField.Name = "key" - val1Field := field - val1Field.Name = "valNotNull" - val2Field := field - val2Field.Name = "valNullable" - val2Field.Nullable = true - - return schema.ObjectType{ - Name: fmt.Sprintf("test_%v", kind), - KeyFields: []schema.Field{keyField}, - ValueFields: []schema.Field{val1Field, val2Field}, - } -} - -var testEnum = schema.EnumDefinition{ - Name: "test_enum_type", - Values: []string{"foo", "bar", "baz"}, -} diff --git a/schema/testing/field.go b/schema/testing/field.go deleted file mode 100644 index 5ea8c202407c..000000000000 --- a/schema/testing/field.go +++ /dev/null @@ -1,177 +0,0 @@ -package schematesting - -import ( - "fmt" - "time" - - "pgregory.net/rapid" - - "cosmossdk.io/schema" -) - -var ( - kindGen = rapid.Map(rapid.IntRange(int(schema.InvalidKind+1), int(schema.MAX_VALID_KIND-1)), - func(i int) schema.Kind { - return schema.Kind(i) - }) - boolGen = rapid.Bool() -) - -// FieldGen generates random Field's based on the validity criteria of fields. -var FieldGen = rapid.Custom(func(t *rapid.T) schema.Field { - kind := kindGen.Draw(t, "kind") - field := schema.Field{ - Name: NameGen.Draw(t, "name"), - Kind: kind, - Nullable: boolGen.Draw(t, "nullable"), - } - - switch kind { - case schema.EnumKind: - field.EnumDefinition = EnumDefinitionGen.Draw(t, "enumDefinition") - case schema.Bech32AddressKind: - field.AddressPrefix = NameGen.Draw(t, "addressPrefix") - default: - } - - return field -}) - -// FieldValueGen generates random valid values for the field, aiming to exercise the full range of possible -// values for the field. -func FieldValueGen(field schema.Field) *rapid.Generator[any] { - gen := baseFieldValue(field) - - if field.Nullable { - return rapid.OneOf(gen, rapid.Just[any](nil)).AsAny() - } - - return gen -} - -func baseFieldValue(field schema.Field) *rapid.Generator[any] { - switch field.Kind { - case schema.StringKind: - return rapid.StringOf(rapid.Rune().Filter(func(r rune) bool { - return r != 0 // filter out NULL characters - })).AsAny() - case schema.BytesKind: - return rapid.SliceOf(rapid.Byte()).AsAny() - case schema.Int8Kind: - return rapid.Int8().AsAny() - case schema.Int16Kind: - return rapid.Int16().AsAny() - case schema.Uint8Kind: - return rapid.Uint8().AsAny() - case schema.Uint16Kind: - return rapid.Uint16().AsAny() - case schema.Int32Kind: - return rapid.Int32().AsAny() - case schema.Uint32Kind: - return rapid.Uint32().AsAny() - case schema.Int64Kind: - return rapid.Int64().AsAny() - case schema.Uint64Kind: - return rapid.Uint64().AsAny() - case schema.Float32Kind: - return rapid.Float32().AsAny() - case schema.Float64Kind: - return rapid.Float64().AsAny() - case schema.IntegerStringKind: - return rapid.StringMatching(schema.IntegerFormat).AsAny() - case schema.DecimalStringKind: - return rapid.StringMatching(schema.DecimalFormat).AsAny() - case schema.BoolKind: - return rapid.Bool().AsAny() - case schema.TimeKind: - return rapid.Map(rapid.Int64(), func(i int64) time.Time { - return time.Unix(0, i) - }).AsAny() - case schema.DurationKind: - return rapid.Map(rapid.Int64(), func(i int64) time.Duration { - return time.Duration(i) - }).AsAny() - case schema.Bech32AddressKind: - return rapid.SliceOfN(rapid.Byte(), 20, 64).AsAny() - case schema.EnumKind: - return rapid.SampledFrom(field.EnumDefinition.Values).AsAny() - default: - panic(fmt.Errorf("unexpected kind: %v", field.Kind)) - } -} - -// KeyFieldsValueGen generates a value that is valid for the provided key fields. -func KeyFieldsValueGen(keyFields []schema.Field) *rapid.Generator[any] { - if len(keyFields) == 0 { - return rapid.Just[any](nil) - } - - if len(keyFields) == 1 { - return FieldValueGen(keyFields[0]) - } - - gens := make([]*rapid.Generator[any], len(keyFields)) - for i, field := range keyFields { - gens[i] = FieldValueGen(field) - } - - return rapid.Custom(func(t *rapid.T) any { - values := make([]any, len(keyFields)) - for i, gen := range gens { - values[i] = gen.Draw(t, keyFields[i].Name) - } - return values - }) -} - -// ValueFieldsValueGen generates a value that is valid for the provided value fields. The -// forUpdate parameter indicates whether the generator should generate value that -// are valid for insertion (in the case forUpdate is false) or for update (in the case forUpdate is true). -// Values that are for update may skip some fields in a ValueUpdates instance whereas values for insertion -// will always contain all values. -func ValueFieldsValueGen(valueFields []schema.Field, forUpdate bool) *rapid.Generator[any] { - // special case where there are no value fields - // we shouldn't end up here, but just in case - if len(valueFields) == 0 { - return rapid.Just[any](nil) - } - - gens := make([]*rapid.Generator[any], len(valueFields)) - for i, field := range valueFields { - gens[i] = FieldValueGen(field) - } - return rapid.Custom(func(t *rapid.T) any { - // return ValueUpdates 50% of the time - if boolGen.Draw(t, "valueUpdates") { - updates := map[string]any{} - - n := len(valueFields) - for i, gen := range gens { - lastField := i == n-1 - haveUpdates := len(updates) > 0 - // skip 50% of the time if this is an update - // but check if we have updates by the time we reach the last field - // so we don't have an empty update - if forUpdate && - (!lastField || haveUpdates) && - boolGen.Draw(t, fmt.Sprintf("skip_%s", valueFields[i].Name)) { - continue - } - updates[valueFields[i].Name] = gen.Draw(t, valueFields[i].Name) - } - - return schema.MapValueUpdates(updates) - } else { - if len(valueFields) == 1 { - return gens[0].Draw(t, valueFields[0].Name) - } - - values := make([]any, len(valueFields)) - for i, gen := range gens { - values[i] = gen.Draw(t, valueFields[i].Name) - } - - return values - } - }) -} diff --git a/schema/testing/field_test.go b/schema/testing/field_test.go deleted file mode 100644 index 2c78ff906372..000000000000 --- a/schema/testing/field_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package schematesting - -import ( - "testing" - "unicode/utf8" - - "github.com/stretchr/testify/require" - "pgregory.net/rapid" -) - -func TestField(t *testing.T) { - rapid.Check(t, func(t *rapid.T) { - field := FieldGen.Draw(t, "field") - require.NoError(t, field.Validate()) - }) -} - -func TestFieldValue(t *testing.T) { - rapid.Check(t, checkFieldValue) -} - -func FuzzFieldValue(f *testing.F) { - strGen := rapid.String() - f.Fuzz(rapid.MakeFuzz(func(t *rapid.T) { - str := strGen.Draw(t, "str") - if !utf8.ValidString(str) { - t.Fatalf("invalid utf8 string: %q", str) - } - })) -} - -var checkFieldValue = func(t *rapid.T) { - field := FieldGen.Draw(t, "field") - require.NoError(t, field.Validate()) - fieldValue := FieldValueGen(field).Draw(t, "fieldValue") - require.NoError(t, field.ValidateValue(fieldValue)) -} diff --git a/schema/testing/go.mod b/schema/testing/go.mod deleted file mode 100644 index e1cfc343bcdf..000000000000 --- a/schema/testing/go.mod +++ /dev/null @@ -1,20 +0,0 @@ -module cosmossdk.io/schema/testing - -require ( - cosmossdk.io/schema v0.0.0 - github.com/stretchr/testify v1.9.0 - github.com/tidwall/btree v1.7.0 - gotest.tools/v3 v3.5.1 - pgregory.net/rapid v1.1.0 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace cosmossdk.io/schema => ./.. - -go 1.22 diff --git a/schema/testing/go.sum b/schema/testing/go.sum deleted file mode 100644 index 393b537c2d4a..000000000000 --- a/schema/testing/go.sum +++ /dev/null @@ -1,18 +0,0 @@ -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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -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/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= -github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= -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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= -pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/schema/testing/module_schema.go b/schema/testing/module_schema.go deleted file mode 100644 index 276b189522c4..000000000000 --- a/schema/testing/module_schema.go +++ /dev/null @@ -1,32 +0,0 @@ -package schematesting - -import ( - "fmt" - - "pgregory.net/rapid" - - "cosmossdk.io/schema" -) - -// ModuleSchemaGen generates random ModuleSchema's based on the validity criteria of module schemas. -var ModuleSchemaGen = rapid.Custom(func(t *rapid.T) schema.ModuleSchema { - schema := schema.ModuleSchema{} - numObjectTypes := rapid.IntRange(1, 10).Draw(t, "numObjectTypes") - for i := 0; i < numObjectTypes; i++ { - objectType := ObjectTypeGen.Draw(t, fmt.Sprintf("objectType[%d]", i)) - schema.ObjectTypes = append(schema.ObjectTypes, objectType) - } - return schema -}).Filter(func(schema schema.ModuleSchema) bool { - // filter out enums with duplicate names - enumTypeNames := map[string]bool{} - for _, objectType := range schema.ObjectTypes { - if !hasDuplicateEnumName(enumTypeNames, objectType.KeyFields) { - return false - } - if !hasDuplicateEnumName(enumTypeNames, objectType.ValueFields) { - return false - } - } - return true -}) diff --git a/schema/testing/module_schema_test.go b/schema/testing/module_schema_test.go deleted file mode 100644 index 91196d59aa18..000000000000 --- a/schema/testing/module_schema_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package schematesting - -import ( - "testing" - - "github.com/stretchr/testify/require" - "pgregory.net/rapid" -) - -func TestModuleSchema(t *testing.T) { - rapid.Check(t, func(t *rapid.T) { - schema := ModuleSchemaGen.Draw(t, "schema") - require.NoError(t, schema.Validate()) - }) -} diff --git a/schema/testing/name.go b/schema/testing/name.go deleted file mode 100644 index 144347e5db46..000000000000 --- a/schema/testing/name.go +++ /dev/null @@ -1,10 +0,0 @@ -package schematesting - -import ( - "pgregory.net/rapid" - - "cosmossdk.io/schema" -) - -// NameGen validates valid names that match the NameFormat regex. -var NameGen = rapid.StringMatching(schema.NameFormat) diff --git a/schema/testing/name_test.go b/schema/testing/name_test.go deleted file mode 100644 index b4d9a44ea612..000000000000 --- a/schema/testing/name_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package schematesting - -import ( - "testing" - - "github.com/stretchr/testify/require" - "pgregory.net/rapid" - - "cosmossdk.io/schema" -) - -func TestName(t *testing.T) { - rapid.Check(t, func(t *rapid.T) { - name := NameGen.Draw(t, "name") - require.True(t, schema.ValidateName(name)) - }) -} diff --git a/schema/testing/object.go b/schema/testing/object.go deleted file mode 100644 index f834d9fb1320..000000000000 --- a/schema/testing/object.go +++ /dev/null @@ -1,128 +0,0 @@ -package schematesting - -import ( - "github.com/tidwall/btree" - "pgregory.net/rapid" - - "cosmossdk.io/schema" -) - -var fieldsGen = rapid.SliceOfNDistinct(FieldGen, 1, 12, func(f schema.Field) string { - return f.Name -}) - -// ObjectTypeGen generates random ObjectType's based on the validity criteria of object types. -var ObjectTypeGen = rapid.Custom(func(t *rapid.T) schema.ObjectType { - typ := schema.ObjectType{ - Name: NameGen.Draw(t, "name"), - } - - fields := fieldsGen.Draw(t, "fields") - numKeyFields := rapid.IntRange(0, len(fields)).Draw(t, "numKeyFields") - - typ.KeyFields = fields[:numKeyFields] - - for i := range typ.KeyFields { - // key fields can't be nullable - typ.KeyFields[i].Nullable = false - } - - typ.ValueFields = fields[numKeyFields:] - - typ.RetainDeletions = boolGen.Draw(t, "retainDeletions") - - return typ -}).Filter(func(typ schema.ObjectType) bool { - // filter out duplicate enum names - enumTypeNames := map[string]bool{} - if hasDuplicateEnumName(enumTypeNames, typ.KeyFields) { - return false - } - if hasDuplicateEnumName(enumTypeNames, typ.ValueFields) { - return false - } - return true -}) - -// hasDuplicateEnumName checks if there is an enum field with a duplicate name -// in the object type -func hasDuplicateEnumName(enumTypeNames map[string]bool, fields []schema.Field) bool { - for _, field := range fields { - if field.Kind != schema.EnumKind { - continue - } - - if _, ok := enumTypeNames[field.EnumDefinition.Name]; ok { - return true - } - - enumTypeNames[field.EnumDefinition.Name] = true - } - return false -} - -// ObjectInsertGen generates object updates that are valid for insertion. -func ObjectInsertGen(objectType schema.ObjectType) *rapid.Generator[schema.ObjectUpdate] { - return ObjectUpdateGen(objectType, nil) -} - -// ObjectUpdateGen generates object updates that are valid for updates using the provided state map as a source -// of valid existing keys. -func ObjectUpdateGen(objectType schema.ObjectType, state *btree.Map[string, schema.ObjectUpdate]) *rapid.Generator[schema.ObjectUpdate] { - keyGen := KeyFieldsValueGen(objectType.KeyFields) - - if len(objectType.ValueFields) == 0 { - // special case where there are no value fields, - // so we just insert or delete, no updates - return rapid.Custom(func(t *rapid.T) schema.ObjectUpdate { - update := schema.ObjectUpdate{ - TypeName: objectType.Name, - } - - // 50% of the time delete existing key (when there are keys) - n := 0 - if state != nil { - n = state.Len() - } - if n > 0 && boolGen.Draw(t, "delete") { - i := rapid.IntRange(0, n-1).Draw(t, "index") - update.Key = state.Values()[i].Key - update.Delete = true - } else { - update.Key = keyGen.Draw(t, "key") - } - - return update - }) - } else { - insertValueGen := ValueFieldsValueGen(objectType.ValueFields, false) - updateValueGen := ValueFieldsValueGen(objectType.ValueFields, true) - return rapid.Custom(func(t *rapid.T) schema.ObjectUpdate { - update := schema.ObjectUpdate{ - TypeName: objectType.Name, - } - - // 50% of the time use existing key (when there are keys) - n := 0 - if state != nil { - n = state.Len() - } - if n > 0 && boolGen.Draw(t, "existingKey") { - i := rapid.IntRange(0, n-1).Draw(t, "index") - update.Key = state.Values()[i].Key - - // delete 50% of the time - if boolGen.Draw(t, "delete") { - update.Delete = true - } else { - update.Value = updateValueGen.Draw(t, "value") - } - } else { - update.Key = keyGen.Draw(t, "key") - update.Value = insertValueGen.Draw(t, "value") - } - - return update - }) - } -} diff --git a/schema/testing/object_test.go b/schema/testing/object_test.go deleted file mode 100644 index 629d8e4764b0..000000000000 --- a/schema/testing/object_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package schematesting - -import ( - "testing" - - "github.com/stretchr/testify/require" - "pgregory.net/rapid" -) - -func TestObject(t *testing.T) { - rapid.Check(t, func(t *rapid.T) { - objectType := ObjectTypeGen.Draw(t, "object") - require.NoError(t, objectType.Validate()) - }) -} - -func TestObjectUpdate(t *testing.T) { - rapid.Check(t, func(t *rapid.T) { - objectType := ObjectTypeGen.Draw(t, "object") - require.NoError(t, objectType.Validate()) - update := ObjectInsertGen(objectType).Draw(t, "update") - require.NoError(t, objectType.ValidateObjectUpdate(update)) - }) -} - -func TestExample(t *testing.T) { - objectType := ObjectTypeGen.Example(1) - update := ObjectInsertGen(objectType).Example(2) - t.Logf("objectType: %+v", objectType) - t.Logf("update: %+v", update) -} diff --git a/schema/testing/statesim/app.go b/schema/testing/statesim/app.go deleted file mode 100644 index 1319e5573e48..000000000000 --- a/schema/testing/statesim/app.go +++ /dev/null @@ -1,93 +0,0 @@ -package statesim - -import ( - "fmt" - - "github.com/stretchr/testify/require" - "github.com/tidwall/btree" - "pgregory.net/rapid" - - "cosmossdk.io/schema" - "cosmossdk.io/schema/appdata" -) - -// App is a collection of simulated module states corresponding to an app's schema for testing purposes. -type App struct { - moduleStates *btree.Map[string, *Module] - updateGen *rapid.Generator[appdata.ObjectUpdateData] -} - -// NewApp creates a new simulation App for the given app schema. -func NewApp(appSchema map[string]schema.ModuleSchema, options Options) *App { - moduleStates := &btree.Map[string, *Module]{} - var moduleNames []string - - for moduleName, moduleSchema := range appSchema { - moduleState := NewModule(moduleSchema, options) - moduleStates.Set(moduleName, moduleState) - moduleNames = append(moduleNames, moduleName) - } - - moduleNameSelector := rapid.Map(rapid.IntRange(0, len(moduleNames)), func(u int) string { - return moduleNames[u] - }) - - numUpdatesGen := rapid.IntRange(1, 2) - updateGen := rapid.Custom(func(t *rapid.T) appdata.ObjectUpdateData { - moduleName := moduleNameSelector.Draw(t, "moduleName") - moduleState, ok := moduleStates.Get(moduleName) - require.True(t, ok) - numUpdates := numUpdatesGen.Draw(t, "numUpdates") - updates := make([]schema.ObjectUpdate, numUpdates) - for i := 0; i < numUpdates; i++ { - update := moduleState.UpdateGen().Draw(t, fmt.Sprintf("update[%d]", i)) - updates[i] = update - } - return appdata.ObjectUpdateData{ - ModuleName: moduleName, - Updates: updates, - } - }) - - return &App{ - moduleStates: moduleStates, - updateGen: updateGen, - } -} - -// ApplyUpdate applies the given object update to the module. -func (a *App) ApplyUpdate(data appdata.ObjectUpdateData) error { - moduleState, ok := a.moduleStates.Get(data.ModuleName) - if !ok { - return fmt.Errorf("module %s not found", data.ModuleName) - } - - for _, update := range data.Updates { - err := moduleState.ApplyUpdate(update) - if err != nil { - return err - } - } - - return nil -} - -// UpdateGen is a generator for object update data against the app. It is stateful and includes a certain number of -// updates and deletions to existing objects. -func (a *App) UpdateGen() *rapid.Generator[appdata.ObjectUpdateData] { - return a.updateGen -} - -// GetModule returns the module state for the given module name. -func (a *App) GetModule(moduleName string) (*Module, bool) { - return a.moduleStates.Get(moduleName) -} - -func (a *App) ScanModules(f func(moduleName string, modState *Module) error) error { - var err error - a.moduleStates.Scan(func(key string, value *Module) bool { - err = f(key, value) - return err == nil - }) - return err -} diff --git a/schema/testing/statesim/doc.go b/schema/testing/statesim/doc.go deleted file mode 100644 index 9f06ff8eb71c..000000000000 --- a/schema/testing/statesim/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package statesim contains utilities for simulating state based on ObjectType's and ModuleSchema's for testing -// the conformance of state management libraries and indexers to schema rules. -package statesim diff --git a/schema/testing/statesim/module.go b/schema/testing/statesim/module.go deleted file mode 100644 index 699cb2924922..000000000000 --- a/schema/testing/statesim/module.go +++ /dev/null @@ -1,82 +0,0 @@ -package statesim - -import ( - "fmt" - - "github.com/stretchr/testify/require" - "github.com/tidwall/btree" - "pgregory.net/rapid" - - "cosmossdk.io/schema" -) - -// Module is a collection of object collections corresponding to a module's schema for testing purposes. -type Module struct { - moduleSchema schema.ModuleSchema - objectCollections *btree.Map[string, *ObjectCollection] - updateGen *rapid.Generator[schema.ObjectUpdate] -} - -// NewModule creates a new Module for the given module schema. -func NewModule(moduleSchema schema.ModuleSchema, options Options) *Module { - objectCollections := &btree.Map[string, *ObjectCollection]{} - var objectTypeNames []string - for _, objectType := range moduleSchema.ObjectTypes { - objectCollection := NewObjectCollection(objectType, options) - objectCollections.Set(objectType.Name, objectCollection) - objectTypeNames = append(objectTypeNames, objectType.Name) - } - - objectTypeSelector := rapid.Map(rapid.IntRange(0, len(objectTypeNames)), func(u int) string { - return objectTypeNames[u] - }) - - updateGen := rapid.Custom(func(t *rapid.T) schema.ObjectUpdate { - objectType := objectTypeSelector.Draw(t, "objectType") - objectColl, ok := objectCollections.Get(objectType) - require.True(t, ok) - return objectColl.UpdateGen().Draw(t, "update") - }) - - return &Module{ - moduleSchema: moduleSchema, - updateGen: updateGen, - objectCollections: objectCollections, - } -} - -// ApplyUpdate applies the given object update to the module. -func (o *Module) ApplyUpdate(update schema.ObjectUpdate) error { - objState, ok := o.objectCollections.Get(update.TypeName) - if !ok { - return fmt.Errorf("object type %s not found in module", update.TypeName) - } - - return objState.ApplyUpdate(update) -} - -// UpdateGen returns a generator for object updates. The generator is stateful and returns -// a certain number of updates and deletes of existing objects in the module. -func (o *Module) UpdateGen() *rapid.Generator[schema.ObjectUpdate] { - return o.updateGen -} - -// ModuleSchema returns the module schema for the module. -func (o *Module) ModuleSchema() schema.ModuleSchema { - return o.moduleSchema -} - -// GetObjectCollection returns the object collection for the given object type. -func (o *Module) GetObjectCollection(objectType string) (*ObjectCollection, bool) { - return o.objectCollections.Get(objectType) -} - -// ScanObjectCollections scans all object collections in the module. -func (o *Module) ScanObjectCollections(f func(value *ObjectCollection) error) error { - var err error - o.objectCollections.Scan(func(key string, value *ObjectCollection) bool { - err = f(value) - return err == nil - }) - return err -} diff --git a/schema/testing/statesim/object.go b/schema/testing/statesim/object.go deleted file mode 100644 index 3c6e95593de4..000000000000 --- a/schema/testing/statesim/object.go +++ /dev/null @@ -1,127 +0,0 @@ -package statesim - -import ( - "fmt" - - "github.com/tidwall/btree" - "pgregory.net/rapid" - - "cosmossdk.io/schema" - schematesting "cosmossdk.io/schema/testing" -) - -// ObjectCollection is a collection of objects of a specific type for testing purposes. -type ObjectCollection struct { - options Options - objectType schema.ObjectType - objects *btree.Map[string, schema.ObjectUpdate] - updateGen *rapid.Generator[schema.ObjectUpdate] - valueFieldIndices map[string]int -} - -// NewObjectCollection creates a new ObjectCollection for the given object type. -func NewObjectCollection(objectType schema.ObjectType, options Options) *ObjectCollection { - objects := &btree.Map[string, schema.ObjectUpdate]{} - updateGen := schematesting.ObjectUpdateGen(objectType, objects) - valueFieldIndices := make(map[string]int, len(objectType.ValueFields)) - for i, field := range objectType.ValueFields { - valueFieldIndices[field.Name] = i - } - - return &ObjectCollection{ - options: options, - objectType: objectType, - objects: objects, - updateGen: updateGen, - valueFieldIndices: valueFieldIndices, - } -} - -// ApplyUpdate applies the given object update to the collection. -func (o *ObjectCollection) ApplyUpdate(update schema.ObjectUpdate) error { - if update.TypeName != o.objectType.Name { - return fmt.Errorf("update type name %q does not match object type name %q", update.TypeName, o.objectType.Name) - } - - err := o.objectType.ValidateObjectUpdate(update) - if err != nil { - return err - } - - keyStr := fmt.Sprintf("%v", update.Key) - cur, exists := o.objects.Get(keyStr) - if update.Delete { - if o.objectType.RetainDeletions && o.options.CanRetainDeletions { - if !exists { - return fmt.Errorf("object not found for deletion: %v", update.Key) - } - - cur.Delete = true - o.objects.Set(keyStr, cur) - } else { - o.objects.Delete(keyStr) - } - } else { - // merge value updates only if we have more than one value field - if valueUpdates, ok := update.Value.(schema.ValueUpdates); ok && - len(o.objectType.ValueFields) > 1 { - var values []interface{} - if exists { - values = cur.Value.([]interface{}) - } else { - values = make([]interface{}, len(o.objectType.ValueFields)) - } - - err = valueUpdates.Iterate(func(fieldName string, value interface{}) bool { - fieldIndex, ok := o.valueFieldIndices[fieldName] - if !ok { - panic(fmt.Sprintf("field %q not found in object type %q", fieldName, o.objectType.Name)) - } - - values[fieldIndex] = value - return true - }) - if err != nil { - return err - } - - update.Value = values - } - - o.objects.Set(keyStr, update) - } - - return nil -} - -// UpdateGen returns a generator for random object updates against the collection. This generator -// is stateful and returns a certain number of updates and deletes to existing objects. -func (o *ObjectCollection) UpdateGen() *rapid.Generator[schema.ObjectUpdate] { - return o.updateGen -} - -// ScanState scans the state of the collection by calling the given function for each object update. -func (o *ObjectCollection) ScanState(f func(schema.ObjectUpdate) error) error { - var err error - o.objects.Scan(func(_ string, v schema.ObjectUpdate) bool { - err = f(v) - return err == nil - }) - return err -} - -// GetObject returns the object with the given key from the collection represented as an ObjectUpdate -// itself. Deletions that are retained are returned as ObjectUpdate's with delete set to true. -func (o *ObjectCollection) GetObject(key any) (update schema.ObjectUpdate, found bool) { - return o.objects.Get(fmt.Sprintf("%v", key)) -} - -// ObjectType returns the object type of the collection. -func (o *ObjectCollection) ObjectType() schema.ObjectType { - return o.objectType -} - -// Len returns the number of objects in the collection. -func (o *ObjectCollection) Len() int { - return o.objects.Len() -} diff --git a/schema/testing/statesim/options.go b/schema/testing/statesim/options.go deleted file mode 100644 index 946138ea7214..000000000000 --- a/schema/testing/statesim/options.go +++ /dev/null @@ -1,5 +0,0 @@ -package statesim - -type Options struct { - CanRetainDeletions bool -} From 74abccddcf30ed994ebb589933e8c16c46025893 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 21:16:39 +0200 Subject: [PATCH 56/79] WIP --- indexer/postgres/column.go | 4 ++-- indexer/postgres/create_table.go | 2 +- indexer/postgres/create_table_test.go | 16 ++++++++-------- indexer/postgres/indexer.go | 10 ---------- indexer/postgres/options.go | 15 +++++++++++++++ indexer/postgres/testing/create_table_test.go | 1 + 6 files changed, 27 insertions(+), 21 deletions(-) delete mode 100644 indexer/postgres/indexer.go create mode 100644 indexer/postgres/options.go create mode 100644 indexer/postgres/testing/create_table_test.go diff --git a/indexer/postgres/column.go b/indexer/postgres/column.go index aa980d385561..9ae55f57cea8 100644 --- a/indexer/postgres/column.go +++ b/indexer/postgres/column.go @@ -87,9 +87,9 @@ func simpleColumnType(kind schema.Kind) string { return "BIGINT" case schema.Uint64Kind: return "NUMERIC" - case schema.IntegerStringKind: + case schema.IntegerKind: return "NUMERIC" - case schema.DecimalStringKind: + case schema.DecimalKind: return "NUMERIC" case schema.Float32Kind: return "REAL" diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index e6331d222209..c00c64aa9a97 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -51,7 +51,7 @@ func (tm *TableManager) CreateTableSql(writer io.Writer) error { } // add _deleted column when we have RetainDeletions set and enabled - if tm.options.RetainDeletions && tm.typ.RetainDeletions { + if tm.options.DisableRetainDeletions && tm.typ.RetainDeletions { _, err = fmt.Fprintf(writer, "_deleted BOOLEAN NOT NULL DEFAULT FALSE,\n\t") if err != nil { return err diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go index 3ddbadb08a8b..3b1b6fa0790c 100644 --- a/indexer/postgres/create_table_test.go +++ b/indexer/postgres/create_table_test.go @@ -8,14 +8,6 @@ import ( "cosmossdk.io/schema/logutil" ) -func exampleCreateTable(objectType schema.ObjectType) { - tm := NewTableManager("test", objectType, Options{Logger: logutil.NoopLogger{}}) - err := tm.CreateTableSql(os.Stdout) - if err != nil { - panic(err) - } -} - func ExampleCreateTable_AllKinds() { exampleCreateTable(testdata.AllKindsObject) // Output: @@ -56,3 +48,11 @@ func ExampleCreateTable_Singleton() { // ); // GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; } + +func exampleCreateTable(objectType schema.ObjectType) { + tm := NewTableManager("test", objectType, Options{Logger: logutil.NoopLogger{}}) + err := tm.CreateTableSql(os.Stdout) + if err != nil { + panic(err) + } +} diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go deleted file mode 100644 index 179bbf23f3cd..000000000000 --- a/indexer/postgres/indexer.go +++ /dev/null @@ -1,10 +0,0 @@ -package postgres - -import ( - "cosmossdk.io/schema/logutil" -) - -type Options struct { - RetainDeletions bool - Logger logutil.Logger -} diff --git a/indexer/postgres/options.go b/indexer/postgres/options.go new file mode 100644 index 000000000000..30256eb75bb2 --- /dev/null +++ b/indexer/postgres/options.go @@ -0,0 +1,15 @@ +package postgres + +import ( + "cosmossdk.io/schema/logutil" +) + +// Options are the options for postgres indexing. +type Options struct { + // DisableRetainDeletions disables retain deletions functionality even on object types that have it set. + // By retaining deletions is supported. + DisableRetainDeletions bool + + // Logger is the logger for the indexer to use. + Logger logutil.Logger +} diff --git a/indexer/postgres/testing/create_table_test.go b/indexer/postgres/testing/create_table_test.go new file mode 100644 index 000000000000..7603f836a002 --- /dev/null +++ b/indexer/postgres/testing/create_table_test.go @@ -0,0 +1 @@ +package testing From b11bb2c415628709f11e909b3f99fee2c102de81 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 21:59:56 +0200 Subject: [PATCH 57/79] integration test --- indexer/postgres/create_table.go | 4 +- indexer/postgres/enum.go | 4 +- indexer/postgres/options.go | 1 - indexer/postgres/testing/create_table_test.go | 1 - indexer/postgres/testing/go.mod | 12 +--- indexer/postgres/testing/go.sum | 6 -- indexer/postgres/testing/init_schema_test.go | 56 +++++++++++++++++++ 7 files changed, 65 insertions(+), 19 deletions(-) delete mode 100644 indexer/postgres/testing/create_table_test.go create mode 100644 indexer/postgres/testing/init_schema_test.go diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index c00c64aa9a97..7e075047490e 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -16,7 +16,9 @@ func (tm *TableManager) CreateTable(ctx context.Context, conn DBConn) error { } sqlStr := buf.String() - tm.options.Logger.Debug("Creating table", "table", tm.TableName(), "sql", sqlStr) + if tm.options.Logger != nil { + tm.options.Logger.Debug("Creating table", "table", tm.TableName(), "sql", sqlStr) + } _, err = conn.ExecContext(ctx, sqlStr) return err } diff --git a/indexer/postgres/enum.go b/indexer/postgres/enum.go index e846452592e0..bb228145c6b1 100644 --- a/indexer/postgres/enum.go +++ b/indexer/postgres/enum.go @@ -31,7 +31,9 @@ func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum sc } sqlStr := buf.String() - m.options.Logger.Debug("Creating enum type", "sql", sqlStr) + if m.options.Logger != nil { + m.options.Logger.Debug("Creating enum type", "sql", sqlStr) + } _, err = conn.ExecContext(ctx, sqlStr) return err } diff --git a/indexer/postgres/options.go b/indexer/postgres/options.go index 30256eb75bb2..51f04af29831 100644 --- a/indexer/postgres/options.go +++ b/indexer/postgres/options.go @@ -7,7 +7,6 @@ import ( // Options are the options for postgres indexing. type Options struct { // DisableRetainDeletions disables retain deletions functionality even on object types that have it set. - // By retaining deletions is supported. DisableRetainDeletions bool // Logger is the logger for the indexer to use. diff --git a/indexer/postgres/testing/create_table_test.go b/indexer/postgres/testing/create_table_test.go deleted file mode 100644 index 7603f836a002..000000000000 --- a/indexer/postgres/testing/create_table_test.go +++ /dev/null @@ -1 +0,0 @@ -package testing diff --git a/indexer/postgres/testing/go.mod b/indexer/postgres/testing/go.mod index 76dbbab8c311..196e241075e6 100644 --- a/indexer/postgres/testing/go.mod +++ b/indexer/postgres/testing/go.mod @@ -1,10 +1,8 @@ module cosmossdk.io/indexer/postgres/testing require ( - cosmossdk.io/indexer/postgres v0.0.0 + cosmossdk.io/indexer/postgres v0.0.0-00010101000000-000000000000 cosmossdk.io/log v1.3.1 - cosmossdk.io/schema v0.0.0 - cosmossdk.io/schema/testing v0.0.0-00010101000000-000000000000 github.com/fergusstrange/embedded-postgres v1.27.0 github.com/hashicorp/consul/sdk v0.16.1 github.com/jackc/pgx/v5 v5.6.0 @@ -12,7 +10,7 @@ require ( ) require ( - github.com/cosmos/btcutil v1.0.5 // indirect + cosmossdk.io/schema v0.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -25,20 +23,16 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/zerolog v1.32.0 // indirect - github.com/tidwall/btree v1.7.0 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - pgregory.net/rapid v1.1.0 // indirect ) -replace cosmossdk.io/schema/testing => ../../../schema/testing +replace cosmossdk.io/schema => ../../../schema replace cosmossdk.io/indexer/postgres => .. -replace cosmossdk.io/schema => ../../../schema - go 1.22 diff --git a/indexer/postgres/testing/go.sum b/indexer/postgres/testing/go.sum index 6f4fc275583d..b41a79b72e9e 100644 --- a/indexer/postgres/testing/go.sum +++ b/indexer/postgres/testing/go.sum @@ -1,8 +1,6 @@ cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= -github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -49,8 +47,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= -github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= @@ -74,5 +70,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= -pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/indexer/postgres/testing/init_schema_test.go b/indexer/postgres/testing/init_schema_test.go new file mode 100644 index 000000000000..95ff9c482de2 --- /dev/null +++ b/indexer/postgres/testing/init_schema_test.go @@ -0,0 +1,56 @@ +package testing + +import ( + "context" + "database/sql" + "os" + "testing" + + embeddedpostgres "github.com/fergusstrange/embedded-postgres" + "github.com/hashicorp/consul/sdk/freeport" + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/stretchr/testify/require" + + "cosmossdk.io/log" + + "cosmossdk.io/indexer/postgres" + "cosmossdk.io/indexer/postgres/internal/testdata" +) + +func TestInitSchema(t *testing.T) { + db := createTestDB(t) + mm := postgres.NewModuleManager("test", testdata.ExampleSchema, postgres.Options{ + Logger: log.NewTestLogger(t), + }) + _, err := db.Exec(postgres.BaseSQL) + require.NoError(t, err) + require.NoError(t, mm.InitializeSchema(context.Background(), db)) +} + +func createTestDB(t *testing.T) *sql.DB { + tempDir, err := os.MkdirTemp("", "postgres-indexer-test") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, os.RemoveAll(tempDir)) + }) + + dbPort := freeport.GetOne(t) + pgConfig := embeddedpostgres.DefaultConfig(). + Port(uint32(dbPort)). + DataPath(tempDir) + + dbUrl := pgConfig.GetConnectionURL() + pg := embeddedpostgres.NewDatabase(pgConfig) + require.NoError(t, pg.Start()) + t.Cleanup(func() { + require.NoError(t, pg.Stop()) + }) + + db, err := sql.Open("pgx", dbUrl) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + return db +} From 64bf68f961b9b8603a1dad9ea383b4cda321d639 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 3 Jul 2024 22:12:11 +0200 Subject: [PATCH 58/79] add READMEs, CHANGELOG.md --- indexer/postgres/CHANGELOG.md | 37 +++++++++++++++++ indexer/postgres/README.md | 41 +++++++++++++++++++ indexer/postgres/tests/README.md | 3 ++ indexer/postgres/{testing => tests}/go.mod | 0 indexer/postgres/{testing => tests}/go.sum | 0 .../{testing => tests}/init_schema_test.go | 2 +- 6 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 indexer/postgres/CHANGELOG.md create mode 100644 indexer/postgres/README.md create mode 100644 indexer/postgres/tests/README.md rename indexer/postgres/{testing => tests}/go.mod (100%) rename indexer/postgres/{testing => tests}/go.sum (100%) rename indexer/postgres/{testing => tests}/init_schema_test.go (98%) diff --git a/indexer/postgres/CHANGELOG.md b/indexer/postgres/CHANGELOG.md new file mode 100644 index 000000000000..0c3c9d03857f --- /dev/null +++ b/indexer/postgres/CHANGELOG.md @@ -0,0 +1,37 @@ + + +# Changelog + +## [Unreleased] diff --git a/indexer/postgres/README.md b/indexer/postgres/README.md new file mode 100644 index 000000000000..a0c0fd1d9b5b --- /dev/null +++ b/indexer/postgres/README.md @@ -0,0 +1,41 @@ +# PostgreSQL Indexer + +The PostgreSQL indexer can fully index current state for all modules that +implement `cosmossdk.io/schema.HasModuleCodec`. + +## Table, Column and Enum Naming + +`ObjectType`s names are converted to table names prefixed with the module name and an underscore. i.e. the `ObjectType` `foo` in module `bar` will be stored in a table named `bar_foo`. + +Column names are identical to field names. All identifiers are quoted with double quotes so that they are case-sensitive and won't clash with any reserved names. + +Like, table names, enum types are prefixed with the module name and an underscore. + +## Schema Type Mapping + +The mapping of `cosmossdk.io/schema` `Kind`s to PostgreSQL types is as follows: + +| Kind | PostgreSQL Type | Notes | +|---------------------|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `StringKind` | `TEXT` | | +| `BoolKind` | `BOOLEAN` | | +| `BytesKind` | `BYTEA` | | +| `Int8Kind` | `SMALLINT` | | +| `Int16Kind` | `SMALLINT` | | +| `Int32Kind` | `INTEGER` | | +| `Int64Kind` | `BIGINT` | | +| `Uint8Kind` | `SMALLINT` | | +| `Uint16Kind` | `INTEGER` | | +| `Uint32Kind` | `BIGINT` | | +| `Uint64Kind` | `NUMERIC` | | +| `Float32Kind` | `REAL` | | +| `Float64Kind` | `DOUBLE PRECISION` | | +| `IntegerStringKind` | `NUMERIC` | | +| `DecimalStringKind` | `NUMERIC` | | +| `JSONKind` | `JSONB` | | +| `Bech32AddressKind` | `TEXT` | addresses are converted to strings with the specified address prefix | +| `TimeKind` | `BIGINT` and `TIMESTAMPTZ` | time types are stored as two columns, one with the `_nanos` suffix with full nano-seconds precision, and another as a `TIMESTAMPTZ` generated column with microsecond precision | +| `DurationKind` | `BIGINT` | durations are stored as a single column in nanoseconds | +| `EnumKind` | `_` | a custom enum type is created for each module prefixed with with module name it pertains to | + + diff --git a/indexer/postgres/tests/README.md b/indexer/postgres/tests/README.md new file mode 100644 index 000000000000..a57c86171104 --- /dev/null +++ b/indexer/postgres/tests/README.md @@ -0,0 +1,3 @@ +# PostgreSQL Indexer Tests + +The majority of tests for the PostgreSQL indexer are stored in this separate `tests` go module to keep the main indexer module free of dependencies on any particular PostgreSQL driver. This allows users to choose their own driver and integrate the indexer free of any dependency conflict concerns. \ No newline at end of file diff --git a/indexer/postgres/testing/go.mod b/indexer/postgres/tests/go.mod similarity index 100% rename from indexer/postgres/testing/go.mod rename to indexer/postgres/tests/go.mod diff --git a/indexer/postgres/testing/go.sum b/indexer/postgres/tests/go.sum similarity index 100% rename from indexer/postgres/testing/go.sum rename to indexer/postgres/tests/go.sum diff --git a/indexer/postgres/testing/init_schema_test.go b/indexer/postgres/tests/init_schema_test.go similarity index 98% rename from indexer/postgres/testing/init_schema_test.go rename to indexer/postgres/tests/init_schema_test.go index 95ff9c482de2..752b936f8151 100644 --- a/indexer/postgres/testing/init_schema_test.go +++ b/indexer/postgres/tests/init_schema_test.go @@ -1,4 +1,4 @@ -package testing +package tests import ( "context" From fcb4febb6b0a1ad8a2bc2f4e95400efcbf1a3946 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 4 Jul 2024 22:22:43 +0200 Subject: [PATCH 59/79] fix --- indexer/postgres/column.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indexer/postgres/column.go b/indexer/postgres/column.go index 9ae55f57cea8..aa980d385561 100644 --- a/indexer/postgres/column.go +++ b/indexer/postgres/column.go @@ -87,9 +87,9 @@ func simpleColumnType(kind schema.Kind) string { return "BIGINT" case schema.Uint64Kind: return "NUMERIC" - case schema.IntegerKind: + case schema.IntegerStringKind: return "NUMERIC" - case schema.DecimalKind: + case schema.DecimalStringKind: return "NUMERIC" case schema.Float32Kind: return "REAL" From 3b97e4c2ed7650a4baf9e4016325ad713d23e8c8 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 12:21:26 +0200 Subject: [PATCH 60/79] add CI config --- .github/dependabot.yml | 18 ++++++++++++ .github/pr_labeler.yml | 2 ++ .github/workflows/test.yml | 34 +++++++++++++++++++++++ go.work.example | 1 + indexer/postgres/sonar-project.properties | 16 +++++++++++ 5 files changed, 71 insertions(+) create mode 100644 indexer/postgres/sonar-project.properties diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1114d14726ce..824c112ee8aa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -106,6 +106,24 @@ updates: labels: - "A:automerge" - dependencies + - package-ecosystem: gomod + directory: "/indexer/postgres" + schedule: + interval: weekly + day: wednesday + time: "01:53" + labels: + - "A:automerge" + - dependencies + - package-ecosystem: gomod + directory: "/indexer/postgres/tests" + schedule: + interval: weekly + day: wednesday + time: "01:53" + labels: + - "A:automerge" + - dependencies - package-ecosystem: gomod directory: "/schema" schedule: diff --git a/.github/pr_labeler.yml b/.github/pr_labeler.yml index 6424808272ec..20b1a79078ce 100644 --- a/.github/pr_labeler.yml +++ b/.github/pr_labeler.yml @@ -24,6 +24,8 @@ - orm/**/* "C:schema": - schema/**/* +"C:indexer/postgres": + - indexer/postgres/**/* "C:x/accounts": - x/accounts/**/* "C:x/auth": diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4daf93be344a..c9939f6ca2c7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -487,6 +487,40 @@ jobs: with: projectBaseDir: schema/ + test-indexer-postgres: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.12" + cache: true + cache-dependency-path: indexer/postgres/tests/go.sum + - uses: technote-space/get-diff-action@v6.1.2 + id: git_diff + with: + PATTERNS: | + indexer/postgres/**/*.go + indexer/postgres/go.mod + indexer/postgres/go.sum + indexer/postgres/tests/go.mod + indexer/postgres/tests/go.sum + - name: tests + if: env.GIT_DIFF + run: | + cd indexer/postgres + go test -mod=readonly -timeout 30m -coverprofile=coverage.out -covermode=atomic ./... + cd tests + go test -mod=readonly -timeout 30m -coverprofile=coverage.out -covermode=atomic ./... + - name: sonarcloud + if: ${{ env.GIT_DIFF && !github.event.pull_request.draft && env.SONAR_TOKEN != null }} + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + projectBaseDir: indexer/postgres/ + test-simapp: runs-on: ubuntu-latest steps: diff --git a/go.work.example b/go.work.example index 5e0f392aad4e..035cbb3e3496 100644 --- a/go.work.example +++ b/go.work.example @@ -9,6 +9,7 @@ use ( ./core/testing ./depinject ./errors + ./indexer/postgres ./log ./math ./orm diff --git a/indexer/postgres/sonar-project.properties b/indexer/postgres/sonar-project.properties new file mode 100644 index 000000000000..6d7366413a15 --- /dev/null +++ b/indexer/postgres/sonar-project.properties @@ -0,0 +1,16 @@ +sonar.projectKey=cosmos-sdk-indexer-postgres +sonar.organization=cosmos + +sonar.projectName=Cosmos SDK - Postgres Indexer +sonar.project.monorepo.enabled=true + +sonar.sources=. +sonar.exclusions=**/*_test.go,**/*.pb.go,**/*.pulsar.go,**/*.pb.gw.go +sonar.coverage.exclusions=**/*_test.go,**/testutil/**,**/*.pb.go,**/*.pb.gw.go,**/*.pulsar.go,test_helpers.go,docs/** +sonar.tests=. +sonar.test.inclusions=**/*_test.go +sonar.go.coverage.reportPaths=coverage.out + +sonar.sourceEncoding=UTF-8 +sonar.scm.provider=git +sonar.scm.forceReloadAll=true From 54f737927fb6cfc018c11ef0cf454f52f260d1b1 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 12:39:11 +0200 Subject: [PATCH 61/79] merge coverage files --- .github/workflows/test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9939f6ca2c7..dcd791169756 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -509,9 +509,11 @@ jobs: if: env.GIT_DIFF run: | cd indexer/postgres - go test -mod=readonly -timeout 30m -coverprofile=coverage.out -covermode=atomic ./... + go test -mod=readonly -timeout 30m -coverprofile=cov1.out -covermode=atomic ./... cd tests - go test -mod=readonly -timeout 30m -coverprofile=coverage.out -covermode=atomic ./... + go test -mod=readonly -timeout 30m -coverprofile=cov2.out -covermode=atomic -coverpkg=cosmossdk.io/indexer/postgres ./... + cd .. + go run github.com/dylandreimerink/gocovmerge/cmd/gocovmerge@latest cov1.out tests/cov2.out > coverage.out - name: sonarcloud if: ${{ env.GIT_DIFF && !github.event.pull_request.draft && env.SONAR_TOKEN != null }} uses: SonarSource/sonarcloud-github-action@master From 3d452443e6b7ee26f06c3a7e46c76aa94fe6262e Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 13:20:35 +0200 Subject: [PATCH 62/79] update indexer API and tests --- indexer/postgres/create_table.go | 4 +- indexer/postgres/create_table_test.go | 3 +- indexer/postgres/enum.go | 2 +- indexer/postgres/indexer.go | 80 +++++++++++++++++++ .../internal/testdata/example_schema.go | 26 ++++++ indexer/postgres/module_mgr.go | 4 +- indexer/postgres/options.go | 10 +-- indexer/postgres/table_mgr.go | 4 +- indexer/postgres/tests/go.mod | 4 +- indexer/postgres/tests/init_schema_test.go | 66 +++++++++++---- .../postgres/tests/testdata/init_schema.txt | 50 ++++++++++++ .../testdata/init_schema_no_retain_delete.txt | 49 ++++++++++++ 12 files changed, 268 insertions(+), 34 deletions(-) create mode 100644 indexer/postgres/indexer.go create mode 100644 indexer/postgres/tests/testdata/init_schema.txt create mode 100644 indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index 7e075047490e..b2d1c487e024 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -17,7 +17,7 @@ func (tm *TableManager) CreateTable(ctx context.Context, conn DBConn) error { sqlStr := buf.String() if tm.options.Logger != nil { - tm.options.Logger.Debug("Creating table", "table", tm.TableName(), "sql", sqlStr) + tm.options.Logger(fmt.Sprintf("Creating table %s", tm.TableName()), sqlStr) } _, err = conn.ExecContext(ctx, sqlStr) return err @@ -53,7 +53,7 @@ func (tm *TableManager) CreateTableSql(writer io.Writer) error { } // add _deleted column when we have RetainDeletions set and enabled - if tm.options.DisableRetainDeletions && tm.typ.RetainDeletions { + if !tm.options.DisableRetainDeletions && tm.typ.RetainDeletions { _, err = fmt.Fprintf(writer, "_deleted BOOLEAN NOT NULL DEFAULT FALSE,\n\t") if err != nil { return err diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go index 3b1b6fa0790c..a62a19929723 100644 --- a/indexer/postgres/create_table_test.go +++ b/indexer/postgres/create_table_test.go @@ -5,7 +5,6 @@ import ( "cosmossdk.io/indexer/postgres/internal/testdata" "cosmossdk.io/schema" - "cosmossdk.io/schema/logutil" ) func ExampleCreateTable_AllKinds() { @@ -50,7 +49,7 @@ func ExampleCreateTable_Singleton() { } func exampleCreateTable(objectType schema.ObjectType) { - tm := NewTableManager("test", objectType, Options{Logger: logutil.NoopLogger{}}) + tm := NewTableManager("test", objectType, ManagerOptions{Logger: func(msg string, sql string, params ...interface{}) {}}) err := tm.CreateTableSql(os.Stdout) if err != nil { panic(err) diff --git a/indexer/postgres/enum.go b/indexer/postgres/enum.go index bb228145c6b1..44d5efcca9e5 100644 --- a/indexer/postgres/enum.go +++ b/indexer/postgres/enum.go @@ -32,7 +32,7 @@ func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum sc sqlStr := buf.String() if m.options.Logger != nil { - m.options.Logger.Debug("Creating enum type", "sql", sqlStr) + m.options.Logger("Creating enum type", sqlStr) } _, err = conn.ExecContext(ctx, sqlStr) return err diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go new file mode 100644 index 000000000000..c131bcf73683 --- /dev/null +++ b/indexer/postgres/indexer.go @@ -0,0 +1,80 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + + "cosmossdk.io/schema/appdata" +) + +type Config struct { + // DatabaseURL is the PostgreSQL connection URL to use to connect to the database. + DatabaseURL string `json:"database_url"` + + // DatabaseDriver is the PostgreSQL database/sql driver to use. This defaults to "pgx". + DatabaseDriver string `json:"database_driver"` + + // DisableRetainDeletions disables the retain deletions functionality even if it is set in an object type schema. + DisableRetainDeletions bool `json:"disable_retain_deletions"` +} + +type SqlLogger = func(msg string, sql string, params ...interface{}) + +func StartIndexer(ctx context.Context, logger SqlLogger, config Config) (appdata.Listener, error) { + if config.DatabaseURL == "" { + return appdata.Listener{}, fmt.Errorf("missing database URL") + } + + driver := config.DatabaseDriver + if driver == "" { + driver = "pgx" + } + + db, err := sql.Open(driver, config.DatabaseURL) + if err != nil { + return appdata.Listener{}, err + } + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return appdata.Listener{}, err + } + + // commit base schema + _, err = tx.Exec(BaseSQL) + if err != nil { + return appdata.Listener{}, err + } + + moduleMgrs := map[string]*ModuleManager{} + mgrOpts := ManagerOptions{ + DisableRetainDeletions: config.DisableRetainDeletions, + Logger: logger, + } + + return appdata.Listener{ + InitializeModuleData: func(data appdata.ModuleInitializationData) error { + moduleName := data.ModuleName + modSchema := data.Schema + _, ok := moduleMgrs[moduleName] + if ok { + return fmt.Errorf("module %s already initialized", moduleName) + } + + mm := NewModuleManager(moduleName, modSchema, mgrOpts) + moduleMgrs[moduleName] = mm + + return mm.InitializeSchema(ctx, tx) + }, + Commit: func(data appdata.CommitData) error { + err = tx.Commit() + if err != nil { + return err + } + + tx, err = db.BeginTx(ctx, nil) + return err + }, + }, nil +} diff --git a/indexer/postgres/internal/testdata/example_schema.go b/indexer/postgres/internal/testdata/example_schema.go index 764ecc3f04d1..2cb174bac9c1 100644 --- a/indexer/postgres/internal/testdata/example_schema.go +++ b/indexer/postgres/internal/testdata/example_schema.go @@ -38,6 +38,7 @@ func init() { ObjectTypes: []schema.ObjectType{ AllKindsObject, SingletonObject, + RetainDeleteObject, }, } } @@ -56,6 +57,31 @@ var SingletonObject = schema.ObjectType{ }, } +var RetainDeleteObject = schema.ObjectType{ + Name: "vote", + KeyFields: []schema.Field{ + { + Name: "proposal", + Kind: schema.Int64Kind, + }, + { + Name: "address", + Kind: schema.Bech32AddressKind, + }, + }, + ValueFields: []schema.Field{ + { + Name: "vote", + Kind: schema.EnumKind, + EnumDefinition: schema.EnumDefinition{ + Name: "vote_type", + Values: []string{"yes", "no", "abstain"}, + }, + }, + }, + RetainDeletions: true, +} + var MyEnum = schema.EnumDefinition{ Name: "my_enum", Values: []string{"a", "b", "c"}, diff --git a/indexer/postgres/module_mgr.go b/indexer/postgres/module_mgr.go index de92d84f7f1f..b513b8d62320 100644 --- a/indexer/postgres/module_mgr.go +++ b/indexer/postgres/module_mgr.go @@ -13,11 +13,11 @@ type ModuleManager struct { schema schema.ModuleSchema tables map[string]*TableManager definedEnums map[string]schema.EnumDefinition - options Options + options ManagerOptions } // NewModuleManager creates a new ModuleManager for the given module schema. -func NewModuleManager(moduleName string, modSchema schema.ModuleSchema, options Options) *ModuleManager { +func NewModuleManager(moduleName string, modSchema schema.ModuleSchema, options ManagerOptions) *ModuleManager { return &ModuleManager{ moduleName: moduleName, schema: modSchema, diff --git a/indexer/postgres/options.go b/indexer/postgres/options.go index 51f04af29831..9d4514a6e713 100644 --- a/indexer/postgres/options.go +++ b/indexer/postgres/options.go @@ -1,14 +1,10 @@ package postgres -import ( - "cosmossdk.io/schema/logutil" -) - -// Options are the options for postgres indexing. -type Options struct { +// ManagerOptions are the options for module and table managers. +type ManagerOptions struct { // DisableRetainDeletions disables retain deletions functionality even on object types that have it set. DisableRetainDeletions bool // Logger is the logger for the indexer to use. - Logger logutil.Logger + Logger SqlLogger } diff --git a/indexer/postgres/table_mgr.go b/indexer/postgres/table_mgr.go index f08eaccbf625..5236514bc930 100644 --- a/indexer/postgres/table_mgr.go +++ b/indexer/postgres/table_mgr.go @@ -12,11 +12,11 @@ type TableManager struct { typ schema.ObjectType valueFields map[string]schema.Field allFields map[string]schema.Field - options Options + options ManagerOptions } // NewTableManager creates a new TableManager for the given object type. -func NewTableManager(moduleName string, typ schema.ObjectType, options Options) *TableManager { +func NewTableManager(moduleName string, typ schema.ObjectType, options ManagerOptions) *TableManager { allFields := make(map[string]schema.Field) valueFields := make(map[string]schema.Field) diff --git a/indexer/postgres/tests/go.mod b/indexer/postgres/tests/go.mod index 196e241075e6..ff5a19dbc1d8 100644 --- a/indexer/postgres/tests/go.mod +++ b/indexer/postgres/tests/go.mod @@ -3,15 +3,17 @@ module cosmossdk.io/indexer/postgres/testing require ( cosmossdk.io/indexer/postgres v0.0.0-00010101000000-000000000000 cosmossdk.io/log v1.3.1 + cosmossdk.io/schema v0.0.0 github.com/fergusstrange/embedded-postgres v1.27.0 github.com/hashicorp/consul/sdk v0.16.1 github.com/jackc/pgx/v5 v5.6.0 github.com/stretchr/testify v1.9.0 + gotest.tools/v3 v3.5.1 ) require ( - cosmossdk.io/schema v0.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect diff --git a/indexer/postgres/tests/init_schema_test.go b/indexer/postgres/tests/init_schema_test.go index 752b936f8151..66f6b702895a 100644 --- a/indexer/postgres/tests/init_schema_test.go +++ b/indexer/postgres/tests/init_schema_test.go @@ -2,32 +2,70 @@ package tests import ( "context" - "database/sql" + "fmt" "os" + "strings" "testing" embeddedpostgres "github.com/fergusstrange/embedded-postgres" "github.com/hashicorp/consul/sdk/freeport" + "gotest.tools/v3/golden" + + // this is where we get our pgx database driver from _ "github.com/jackc/pgx/v5/stdlib" - "github.com/stretchr/testify/require" - "cosmossdk.io/log" + "github.com/stretchr/testify/require" "cosmossdk.io/indexer/postgres" "cosmossdk.io/indexer/postgres/internal/testdata" + "cosmossdk.io/schema/appdata" ) func TestInitSchema(t *testing.T) { - db := createTestDB(t) - mm := postgres.NewModuleManager("test", testdata.ExampleSchema, postgres.Options{ - Logger: log.NewTestLogger(t), + t.Run("default", func(t *testing.T) { + testInitSchema(t, false, "init_schema.txt") + }) + + t.Run("retain deletions disabled", func(t *testing.T) { + testInitSchema(t, true, "init_schema_no_retain_delete.txt") + }) +} + +func testInitSchema(t *testing.T, disableRetainDeletions bool, goldenFileName string) { + connectionUrl := createTestDB(t) + + buf := &strings.Builder{} + var logger = func(msg string, sql string, params ...interface{}) { + _, err := fmt.Fprintln(buf, msg) + require.NoError(t, err) + _, err = fmt.Fprintln(buf, sql) + require.NoError(t, err) + if len(params) != 0 { + _, err = fmt.Fprintln(buf, "Params:", params) + require.NoError(t, err) + } + _, err = fmt.Fprintln(buf) + require.NoError(t, err) + } + listener, err := postgres.StartIndexer(context.Background(), logger, postgres.Config{ + DatabaseURL: connectionUrl, + DisableRetainDeletions: disableRetainDeletions, }) - _, err := db.Exec(postgres.BaseSQL) require.NoError(t, err) - require.NoError(t, mm.InitializeSchema(context.Background(), db)) + + require.NotNil(t, listener.InitializeModuleData) + require.NoError(t, listener.InitializeModuleData(appdata.ModuleInitializationData{ + ModuleName: "test", + Schema: testdata.ExampleSchema, + })) + + require.NotNil(t, listener.Commit) + require.NoError(t, listener.Commit(appdata.CommitData{})) + + golden.Assert(t, buf.String(), goldenFileName) } -func createTestDB(t *testing.T) *sql.DB { +func createTestDB(t *testing.T) (connectionUrl string) { tempDir, err := os.MkdirTemp("", "postgres-indexer-test") require.NoError(t, err) t.Cleanup(func() { @@ -39,18 +77,12 @@ func createTestDB(t *testing.T) *sql.DB { Port(uint32(dbPort)). DataPath(tempDir) - dbUrl := pgConfig.GetConnectionURL() + connectionUrl = pgConfig.GetConnectionURL() pg := embeddedpostgres.NewDatabase(pgConfig) require.NoError(t, pg.Start()) t.Cleanup(func() { require.NoError(t, pg.Stop()) }) - db, err := sql.Open("pgx", dbUrl) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, db.Close()) - }) - - return db + return } diff --git a/indexer/postgres/tests/testdata/init_schema.txt b/indexer/postgres/tests/testdata/init_schema.txt new file mode 100644 index 000000000000..0fbae9b1c502 --- /dev/null +++ b/indexer/postgres/tests/testdata/init_schema.txt @@ -0,0 +1,50 @@ +Creating enum type +CREATE TYPE "test_my_enum" AS ENUM ('a', 'b', 'c'); + +Creating enum type +CREATE TYPE "test_vote_type" AS ENUM ('yes', 'no', 'abstain'); + +Creating table test_all_kinds +CREATE TABLE IF NOT EXISTS "test_all_kinds" ("id" BIGINT NOT NULL, + "string" TEXT NOT NULL, + "bytes" BYTEA NOT NULL, + "int8" SMALLINT NOT NULL, + "uint8" SMALLINT NOT NULL, + "int16" SMALLINT NOT NULL, + "uint16" INTEGER NOT NULL, + "int32" INTEGER NOT NULL, + "uint32" BIGINT NOT NULL, + "int64" BIGINT NOT NULL, + "uint64" NUMERIC NOT NULL, + "integer" NUMERIC NOT NULL, + "decimal" NUMERIC NOT NULL, + "bool" BOOLEAN NOT NULL, + "time" TIMESTAMPTZ GENERATED ALWAYS AS (nanos_to_timestamptz("time_nanos")) STORED, + "time_nanos" BIGINT NOT NULL, + "duration" BIGINT NOT NULL, + "float32" REAL NOT NULL, + "float64" DOUBLE PRECISION NOT NULL, + "bech32address" TEXT NOT NULL, + "enum" "test_my_enum" NOT NULL, + "json" JSONB NOT NULL, + PRIMARY KEY ("id") +); +GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; + +Creating table test_singleton +CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), + "foo" TEXT NOT NULL, + "bar" INTEGER NOT NULL, + PRIMARY KEY (_id) +); +GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; + +Creating table test_vote +CREATE TABLE IF NOT EXISTS "test_vote" ("proposal" BIGINT NOT NULL, + "address" TEXT NOT NULL, + "vote" "test_vote_type" NOT NULL, + _deleted BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY ("proposal", "address") +); +GRANT SELECT ON TABLE "test_vote" TO PUBLIC; + diff --git a/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt b/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt new file mode 100644 index 000000000000..398c76afbd70 --- /dev/null +++ b/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt @@ -0,0 +1,49 @@ +Creating enum type +CREATE TYPE "test_my_enum" AS ENUM ('a', 'b', 'c'); + +Creating enum type +CREATE TYPE "test_vote_type" AS ENUM ('yes', 'no', 'abstain'); + +Creating table test_all_kinds +CREATE TABLE IF NOT EXISTS "test_all_kinds" ("id" BIGINT NOT NULL, + "string" TEXT NOT NULL, + "bytes" BYTEA NOT NULL, + "int8" SMALLINT NOT NULL, + "uint8" SMALLINT NOT NULL, + "int16" SMALLINT NOT NULL, + "uint16" INTEGER NOT NULL, + "int32" INTEGER NOT NULL, + "uint32" BIGINT NOT NULL, + "int64" BIGINT NOT NULL, + "uint64" NUMERIC NOT NULL, + "integer" NUMERIC NOT NULL, + "decimal" NUMERIC NOT NULL, + "bool" BOOLEAN NOT NULL, + "time" TIMESTAMPTZ GENERATED ALWAYS AS (nanos_to_timestamptz("time_nanos")) STORED, + "time_nanos" BIGINT NOT NULL, + "duration" BIGINT NOT NULL, + "float32" REAL NOT NULL, + "float64" DOUBLE PRECISION NOT NULL, + "bech32address" TEXT NOT NULL, + "enum" "test_my_enum" NOT NULL, + "json" JSONB NOT NULL, + PRIMARY KEY ("id") +); +GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; + +Creating table test_singleton +CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), + "foo" TEXT NOT NULL, + "bar" INTEGER NOT NULL, + PRIMARY KEY (_id) +); +GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; + +Creating table test_vote +CREATE TABLE IF NOT EXISTS "test_vote" ("proposal" BIGINT NOT NULL, + "address" TEXT NOT NULL, + "vote" "test_vote_type" NOT NULL, + PRIMARY KEY ("proposal", "address") +); +GRANT SELECT ON TABLE "test_vote" TO PUBLIC; + From a02093f54ab3321fa266d65c5211e2f267e21a69 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 13:21:07 +0200 Subject: [PATCH 63/79] test fix --- indexer/postgres/enum_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/indexer/postgres/enum_test.go b/indexer/postgres/enum_test.go index abae7f252001..157ee8e6cf7d 100644 --- a/indexer/postgres/enum_test.go +++ b/indexer/postgres/enum_test.go @@ -7,7 +7,10 @@ import ( ) func ExampleCreateEnumType() { - CreateEnumTypeSql(os.Stdout, "test", testdata.MyEnum) + err := CreateEnumTypeSql(os.Stdout, "test", testdata.MyEnum) + if err != nil { + panic(err) + } // Output: // CREATE TYPE "test_my_enum" AS ENUM ('a', 'b', 'c'); } From e1a8ce01ea5b272fcd7dde9c743e9d76730a747a Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 14:35:25 +0200 Subject: [PATCH 64/79] fix %w --- indexer/postgres/enum.go | 2 +- indexer/postgres/module_mgr.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/indexer/postgres/enum.go b/indexer/postgres/enum.go index 44d5efcca9e5..2f77e0d4a757 100644 --- a/indexer/postgres/enum.go +++ b/indexer/postgres/enum.go @@ -17,7 +17,7 @@ func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum sc var res interface{} if err := row.Scan(&res); err != nil { if err != sql.ErrNoRows { - return fmt.Errorf("failed to check if enum type %q exists: %w", typeName, err) + return fmt.Errorf("failed to check if enum type %q exists: %v", typeName, err) } } else { // the enum type already exists diff --git a/indexer/postgres/module_mgr.go b/indexer/postgres/module_mgr.go index b513b8d62320..dc9edda5cf67 100644 --- a/indexer/postgres/module_mgr.go +++ b/indexer/postgres/module_mgr.go @@ -48,7 +48,7 @@ func (m *ModuleManager) InitializeSchema(ctx context.Context, conn DBConn) error m.tables[typ.Name] = tm err := tm.CreateTable(ctx, conn) if err != nil { - return fmt.Errorf("failed to create table for %s in module %s: %w", typ.Name, m.moduleName, err) + return fmt.Errorf("failed to create table for %s in module %s: %v", typ.Name, m.moduleName, err) } } From 844ac5b44071921c18e3e517ec25405e49bccdb5 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 14:40:03 +0200 Subject: [PATCH 65/79] go mod tidy --- indexer/postgres/go.sum | 2 -- indexer/postgres/tests/go.mod | 13 ++++--------- indexer/postgres/tests/go.sum | 21 +++------------------ 3 files changed, 7 insertions(+), 29 deletions(-) diff --git a/indexer/postgres/go.sum b/indexer/postgres/go.sum index fa4b37f95d3b..e69de29bb2d1 100644 --- a/indexer/postgres/go.sum +++ b/indexer/postgres/go.sum @@ -1,2 +0,0 @@ -github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= -github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= diff --git a/indexer/postgres/tests/go.mod b/indexer/postgres/tests/go.mod index ff5a19dbc1d8..07a4b6861c10 100644 --- a/indexer/postgres/tests/go.mod +++ b/indexer/postgres/tests/go.mod @@ -2,7 +2,6 @@ module cosmossdk.io/indexer/postgres/testing require ( cosmossdk.io/indexer/postgres v0.0.0-00010101000000-000000000000 - cosmossdk.io/log v1.3.1 cosmossdk.io/schema v0.0.0 github.com/fergusstrange/embedded-postgres v1.27.0 github.com/hashicorp/consul/sdk v0.16.1 @@ -19,22 +18,18 @@ require ( github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.4 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/rs/zerolog v1.32.0 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.23.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace cosmossdk.io/schema => ../../../schema -replace cosmossdk.io/indexer/postgres => .. +replace cosmossdk.io/indexer/postgres => ../. go 1.22 diff --git a/indexer/postgres/tests/go.sum b/indexer/postgres/tests/go.sum index b41a79b72e9e..9e99ff911a52 100644 --- a/indexer/postgres/tests/go.sum +++ b/indexer/postgres/tests/go.sum @@ -1,13 +1,9 @@ -cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= -cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fergusstrange/embedded-postgres v1.27.0 h1:RAlpWL194IhEpPgeJceTM0ifMJKhiSVxBVIDYB1Jee8= github.com/fergusstrange/embedded-postgres v1.27.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= @@ -26,22 +22,11 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -53,15 +38,15 @@ go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 2784347fa125c4629785f4fe536a4c4227462648 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 14:48:29 +0200 Subject: [PATCH 66/79] go mod tidy --- indexer/postgres/tests/go.sum | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/indexer/postgres/tests/go.sum b/indexer/postgres/tests/go.sum index 9e99ff911a52..de3d49d74702 100644 --- a/indexer/postgres/tests/go.sum +++ b/indexer/postgres/tests/go.sum @@ -36,16 +36,13 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From 04fd8b40b43dbf8164ffa590f5fe4805bb186703 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 14:53:29 +0200 Subject: [PATCH 67/79] lint, build updates --- .github/workflows/test.yml | 8 ++++---- indexer/postgres/create_table_test.go | 4 ++-- indexer/postgres/enum_test.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dcd791169756..8c317ca22c89 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -493,7 +493,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.12" + go-version: "1.22" cache: true cache-dependency-path: indexer/postgres/tests/go.sum - uses: technote-space/get-diff-action@v6.1.2 @@ -509,11 +509,11 @@ jobs: if: env.GIT_DIFF run: | cd indexer/postgres - go test -mod=readonly -timeout 30m -coverprofile=cov1.out -covermode=atomic ./... + go test -mod=readonly -timeout 30m -coverprofile=cov.out -covermode=atomic ./... cd tests - go test -mod=readonly -timeout 30m -coverprofile=cov2.out -covermode=atomic -coverpkg=cosmossdk.io/indexer/postgres ./... + go test -mod=readonly -timeout 30m -coverprofile=cov.out -covermode=atomic -coverpkg=cosmossdk.io/indexer/postgres ./... cd .. - go run github.com/dylandreimerink/gocovmerge/cmd/gocovmerge@latest cov1.out tests/cov2.out > coverage.out + go run github.com/dylandreimerink/gocovmerge/cmd/gocovmerge@latest cov.out tests/cov.out > coverage.out - name: sonarcloud if: ${{ env.GIT_DIFF && !github.event.pull_request.draft && env.SONAR_TOKEN != null }} uses: SonarSource/sonarcloud-github-action@master diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go index a62a19929723..1edfae4fbc01 100644 --- a/indexer/postgres/create_table_test.go +++ b/indexer/postgres/create_table_test.go @@ -7,7 +7,7 @@ import ( "cosmossdk.io/schema" ) -func ExampleCreateTable_AllKinds() { +func ExampleTableManager_CreateTableSql_allKinds() { exampleCreateTable(testdata.AllKindsObject) // Output: // CREATE TABLE IF NOT EXISTS "test_all_kinds" ("id" BIGINT NOT NULL, @@ -37,7 +37,7 @@ func ExampleCreateTable_AllKinds() { // GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; } -func ExampleCreateTable_Singleton() { +func ExampleTableManager_CreateTableSql_singleton() { exampleCreateTable(testdata.SingletonObject) // Output: // CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), diff --git a/indexer/postgres/enum_test.go b/indexer/postgres/enum_test.go index 157ee8e6cf7d..22d8870171c3 100644 --- a/indexer/postgres/enum_test.go +++ b/indexer/postgres/enum_test.go @@ -6,7 +6,7 @@ import ( "cosmossdk.io/indexer/postgres/internal/testdata" ) -func ExampleCreateEnumType() { +func ExampleCreateEnumTypeSql() { err := CreateEnumTypeSql(os.Stdout, "test", testdata.MyEnum) if err != nil { panic(err) From 8c79a6ce313357122a6aa34ab541003bde09ac2d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 15:03:07 +0200 Subject: [PATCH 68/79] lint --- indexer/postgres/column.go | 1 + indexer/postgres/enum.go | 3 ++- indexer/postgres/module_mgr.go | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/indexer/postgres/column.go b/indexer/postgres/column.go index aa980d385561..a2e65ea0b6db 100644 --- a/indexer/postgres/column.go +++ b/indexer/postgres/column.go @@ -64,6 +64,7 @@ func writeNullability(writer io.Writer, nullable bool) error { // simpleColumnType returns the postgres column type for the kind for simple types. func simpleColumnType(kind schema.Kind) string { + //nolint:goconst switch kind { case schema.StringKind: return "TEXT" diff --git a/indexer/postgres/enum.go b/indexer/postgres/enum.go index 2f77e0d4a757..7799e23d85f3 100644 --- a/indexer/postgres/enum.go +++ b/indexer/postgres/enum.go @@ -17,7 +17,8 @@ func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum sc var res interface{} if err := row.Scan(&res); err != nil { if err != sql.ErrNoRows { - return fmt.Errorf("failed to check if enum type %q exists: %v", typeName, err) + // use %v instead of %w for go 1.12 compat + return fmt.Errorf("failed to check if enum type %q exists: %v", typeName, err) //nolint:errorlint } } else { // the enum type already exists diff --git a/indexer/postgres/module_mgr.go b/indexer/postgres/module_mgr.go index dc9edda5cf67..f7ab937a1798 100644 --- a/indexer/postgres/module_mgr.go +++ b/indexer/postgres/module_mgr.go @@ -48,7 +48,8 @@ func (m *ModuleManager) InitializeSchema(ctx context.Context, conn DBConn) error m.tables[typ.Name] = tm err := tm.CreateTable(ctx, conn) if err != nil { - return fmt.Errorf("failed to create table for %s in module %s: %v", typ.Name, m.moduleName, err) + // use %v instead of %w for go 1.12 compat + return fmt.Errorf("failed to create table for %s in module %s: %v", typ.Name, m.moduleName, err) //nolint:errorlint } } From b48d768ecec7d3f3d96fcc0500ab15ad524b2171 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 15:08:44 +0200 Subject: [PATCH 69/79] lint --- indexer/postgres/column.go | 2 +- indexer/postgres/enum.go | 3 +-- indexer/postgres/module_mgr.go | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/indexer/postgres/column.go b/indexer/postgres/column.go index a2e65ea0b6db..b5a2b47613c1 100644 --- a/indexer/postgres/column.go +++ b/indexer/postgres/column.go @@ -64,7 +64,7 @@ func writeNullability(writer io.Writer, nullable bool) error { // simpleColumnType returns the postgres column type for the kind for simple types. func simpleColumnType(kind schema.Kind) string { - //nolint:goconst + //nolint:goconst // adding constants for these postgres type names would impede readability switch kind { case schema.StringKind: return "TEXT" diff --git a/indexer/postgres/enum.go b/indexer/postgres/enum.go index 7799e23d85f3..eb108c2db796 100644 --- a/indexer/postgres/enum.go +++ b/indexer/postgres/enum.go @@ -17,8 +17,7 @@ func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum sc var res interface{} if err := row.Scan(&res); err != nil { if err != sql.ErrNoRows { - // use %v instead of %w for go 1.12 compat - return fmt.Errorf("failed to check if enum type %q exists: %v", typeName, err) //nolint:errorlint + return fmt.Errorf("failed to check if enum type %q exists: %v", typeName, err) //nolint:errorlint // using %v for go 1.12 compat } } else { // the enum type already exists diff --git a/indexer/postgres/module_mgr.go b/indexer/postgres/module_mgr.go index f7ab937a1798..3ab673f63939 100644 --- a/indexer/postgres/module_mgr.go +++ b/indexer/postgres/module_mgr.go @@ -48,8 +48,7 @@ func (m *ModuleManager) InitializeSchema(ctx context.Context, conn DBConn) error m.tables[typ.Name] = tm err := tm.CreateTable(ctx, conn) if err != nil { - // use %v instead of %w for go 1.12 compat - return fmt.Errorf("failed to create table for %s in module %s: %v", typ.Name, m.moduleName, err) //nolint:errorlint + return fmt.Errorf("failed to create table for %s in module %s: %v", typ.Name, m.moduleName, err) //nolint:errorlint // using %v for go 1.12 compat } } From 7c224129756b66af80469be27df9914cfb551d3c Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 15:21:58 +0200 Subject: [PATCH 70/79] increase test coverage, gofumpt --- indexer/postgres/create_table_test.go | 34 +++++++++++++++++-- indexer/postgres/indexer.go | 2 +- .../internal/testdata/example_schema.go | 9 ++--- indexer/postgres/module_mgr.go | 1 - indexer/postgres/tests/init_schema_test.go | 2 +- .../postgres/tests/testdata/init_schema.txt | 2 +- .../testdata/init_schema_no_retain_delete.txt | 2 +- 7 files changed, 41 insertions(+), 11 deletions(-) diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go index 1edfae4fbc01..f72c33c667e0 100644 --- a/indexer/postgres/create_table_test.go +++ b/indexer/postgres/create_table_test.go @@ -42,14 +42,44 @@ func ExampleTableManager_CreateTableSql_singleton() { // Output: // CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), // "foo" TEXT NOT NULL, - // "bar" INTEGER NOT NULL, + // "bar" INTEGER NULL, // PRIMARY KEY (_id) // ); // GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; } +func ExampleTableManager_CreateTableSql_vote() { + exampleCreateTable(testdata.VoteObject) + // Output: + // CREATE TABLE IF NOT EXISTS "test_vote" ("proposal" BIGINT NOT NULL, + // "address" TEXT NOT NULL, + // "vote" "test_vote_type" NOT NULL, + // _deleted BOOLEAN NOT NULL DEFAULT FALSE, + // PRIMARY KEY ("proposal", "address") + // ); + // GRANT SELECT ON TABLE "test_vote" TO PUBLIC; +} + +func ExampleTableManager_CreateTableSql_vote_no_retain_delete() { + exampleCreateTableOpt(testdata.VoteObject, true) + // Output: + // CREATE TABLE IF NOT EXISTS "test_vote" ("proposal" BIGINT NOT NULL, + // "address" TEXT NOT NULL, + // "vote" "test_vote_type" NOT NULL, + // PRIMARY KEY ("proposal", "address") + // ); + // GRANT SELECT ON TABLE "test_vote" TO PUBLIC; +} + func exampleCreateTable(objectType schema.ObjectType) { - tm := NewTableManager("test", objectType, ManagerOptions{Logger: func(msg string, sql string, params ...interface{}) {}}) + exampleCreateTableOpt(objectType, false) +} + +func exampleCreateTableOpt(objectType schema.ObjectType, noRetainDelete bool) { + tm := NewTableManager("test", objectType, ManagerOptions{ + Logger: func(msg, sql string, params ...interface{}) {}, + DisableRetainDeletions: noRetainDelete, + }) err := tm.CreateTableSql(os.Stdout) if err != nil { panic(err) diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index c131bcf73683..60951bec10c3 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -19,7 +19,7 @@ type Config struct { DisableRetainDeletions bool `json:"disable_retain_deletions"` } -type SqlLogger = func(msg string, sql string, params ...interface{}) +type SqlLogger = func(msg, sql string, params ...interface{}) func StartIndexer(ctx context.Context, logger SqlLogger, config Config) (appdata.Listener, error) { if config.DatabaseURL == "" { diff --git a/indexer/postgres/internal/testdata/example_schema.go b/indexer/postgres/internal/testdata/example_schema.go index 2cb174bac9c1..4533a889963e 100644 --- a/indexer/postgres/internal/testdata/example_schema.go +++ b/indexer/postgres/internal/testdata/example_schema.go @@ -38,7 +38,7 @@ func init() { ObjectTypes: []schema.ObjectType{ AllKindsObject, SingletonObject, - RetainDeleteObject, + VoteObject, }, } } @@ -51,13 +51,14 @@ var SingletonObject = schema.ObjectType{ Kind: schema.StringKind, }, { - Name: "bar", - Kind: schema.Int32Kind, + Name: "bar", + Kind: schema.Int32Kind, + Nullable: true, }, }, } -var RetainDeleteObject = schema.ObjectType{ +var VoteObject = schema.ObjectType{ Name: "vote", KeyFields: []schema.Field{ { diff --git a/indexer/postgres/module_mgr.go b/indexer/postgres/module_mgr.go index 3ab673f63939..09429a1540bf 100644 --- a/indexer/postgres/module_mgr.go +++ b/indexer/postgres/module_mgr.go @@ -53,7 +53,6 @@ func (m *ModuleManager) InitializeSchema(ctx context.Context, conn DBConn) error } return nil - } // Tables returns the table managers for the module. diff --git a/indexer/postgres/tests/init_schema_test.go b/indexer/postgres/tests/init_schema_test.go index 66f6b702895a..c77fa7fecd78 100644 --- a/indexer/postgres/tests/init_schema_test.go +++ b/indexer/postgres/tests/init_schema_test.go @@ -35,7 +35,7 @@ func testInitSchema(t *testing.T, disableRetainDeletions bool, goldenFileName st connectionUrl := createTestDB(t) buf := &strings.Builder{} - var logger = func(msg string, sql string, params ...interface{}) { + logger := func(msg, sql string, params ...interface{}) { _, err := fmt.Fprintln(buf, msg) require.NoError(t, err) _, err = fmt.Fprintln(buf, sql) diff --git a/indexer/postgres/tests/testdata/init_schema.txt b/indexer/postgres/tests/testdata/init_schema.txt index 0fbae9b1c502..ff64548cf155 100644 --- a/indexer/postgres/tests/testdata/init_schema.txt +++ b/indexer/postgres/tests/testdata/init_schema.txt @@ -34,7 +34,7 @@ GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; Creating table test_singleton CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), "foo" TEXT NOT NULL, - "bar" INTEGER NOT NULL, + "bar" INTEGER NULL, PRIMARY KEY (_id) ); GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; diff --git a/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt b/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt index 398c76afbd70..3a476d4be6d5 100644 --- a/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt +++ b/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt @@ -34,7 +34,7 @@ GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; Creating table test_singleton CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), "foo" TEXT NOT NULL, - "bar" INTEGER NOT NULL, + "bar" INTEGER NULL, PRIMARY KEY (_id) ); GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; From 28d8413a6dfd161b5e5c5b35a3e19a323c218b1e Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 15:33:48 +0200 Subject: [PATCH 71/79] increase test coverage --- indexer/postgres/create_table_test.go | 1 + indexer/postgres/internal/testdata/example_schema.go | 5 +++++ indexer/postgres/tests/testdata/init_schema.txt | 1 + .../postgres/tests/testdata/init_schema_no_retain_delete.txt | 1 + 4 files changed, 8 insertions(+) diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go index f72c33c667e0..1ee663e6ad5b 100644 --- a/indexer/postgres/create_table_test.go +++ b/indexer/postgres/create_table_test.go @@ -43,6 +43,7 @@ func ExampleTableManager_CreateTableSql_singleton() { // CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), // "foo" TEXT NOT NULL, // "bar" INTEGER NULL, + // "an_enum" "test_my_enum" NOT NULL, // PRIMARY KEY (_id) // ); // GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; diff --git a/indexer/postgres/internal/testdata/example_schema.go b/indexer/postgres/internal/testdata/example_schema.go index 4533a889963e..232dd5a3cc3b 100644 --- a/indexer/postgres/internal/testdata/example_schema.go +++ b/indexer/postgres/internal/testdata/example_schema.go @@ -55,6 +55,11 @@ var SingletonObject = schema.ObjectType{ Kind: schema.Int32Kind, Nullable: true, }, + { + Name: "an_enum", + Kind: schema.EnumKind, + EnumDefinition: MyEnum, + }, }, } diff --git a/indexer/postgres/tests/testdata/init_schema.txt b/indexer/postgres/tests/testdata/init_schema.txt index ff64548cf155..f094cdb5523b 100644 --- a/indexer/postgres/tests/testdata/init_schema.txt +++ b/indexer/postgres/tests/testdata/init_schema.txt @@ -35,6 +35,7 @@ Creating table test_singleton CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), "foo" TEXT NOT NULL, "bar" INTEGER NULL, + "an_enum" "test_my_enum" NOT NULL, PRIMARY KEY (_id) ); GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; diff --git a/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt b/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt index 3a476d4be6d5..341e11e67501 100644 --- a/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt +++ b/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt @@ -35,6 +35,7 @@ Creating table test_singleton CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), "foo" TEXT NOT NULL, "bar" INTEGER NULL, + "an_enum" "test_my_enum" NOT NULL, PRIMARY KEY (_id) ); GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; From d1dd8bdaa2ab5bedca6cdb40b851365a644cdc51 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 15:38:14 +0200 Subject: [PATCH 72/79] Update indexer/postgres/README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- indexer/postgres/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indexer/postgres/README.md b/indexer/postgres/README.md index a0c0fd1d9b5b..05b5eaecb6ee 100644 --- a/indexer/postgres/README.md +++ b/indexer/postgres/README.md @@ -1,6 +1,6 @@ # PostgreSQL Indexer -The PostgreSQL indexer can fully index current state for all modules that +The PostgreSQL indexer can fully index the current state for all modules that implement `cosmossdk.io/schema.HasModuleCodec`. implement `cosmossdk.io/schema.HasModuleCodec`. ## Table, Column and Enum Naming From f7c592ca048350d24969247387d3730b997d8dcb Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 15:38:28 +0200 Subject: [PATCH 73/79] Update indexer/postgres/README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- indexer/postgres/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indexer/postgres/README.md b/indexer/postgres/README.md index 05b5eaecb6ee..c620779b943c 100644 --- a/indexer/postgres/README.md +++ b/indexer/postgres/README.md @@ -36,6 +36,6 @@ The mapping of `cosmossdk.io/schema` `Kind`s to PostgreSQL types is as follows: | `Bech32AddressKind` | `TEXT` | addresses are converted to strings with the specified address prefix | | `TimeKind` | `BIGINT` and `TIMESTAMPTZ` | time types are stored as two columns, one with the `_nanos` suffix with full nano-seconds precision, and another as a `TIMESTAMPTZ` generated column with microsecond precision | | `DurationKind` | `BIGINT` | durations are stored as a single column in nanoseconds | -| `EnumKind` | `_` | a custom enum type is created for each module prefixed with with module name it pertains to | +| `EnumKind` | `_` | a custom enum type is created for each module prefixed with the module name it pertains to | From 0d40d7671e1240336a92ab1d6955278ca6922df2 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 8 Jul 2024 15:41:30 +0200 Subject: [PATCH 74/79] Update indexer/postgres/README.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- indexer/postgres/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indexer/postgres/README.md b/indexer/postgres/README.md index c620779b943c..bb8c480f664e 100644 --- a/indexer/postgres/README.md +++ b/indexer/postgres/README.md @@ -34,7 +34,7 @@ The mapping of `cosmossdk.io/schema` `Kind`s to PostgreSQL types is as follows: | `DecimalStringKind` | `NUMERIC` | | | `JSONKind` | `JSONB` | | | `Bech32AddressKind` | `TEXT` | addresses are converted to strings with the specified address prefix | -| `TimeKind` | `BIGINT` and `TIMESTAMPTZ` | time types are stored as two columns, one with the `_nanos` suffix with full nano-seconds precision, and another as a `TIMESTAMPTZ` generated column with microsecond precision | +| `TimeKind` | `BIGINT` and `TIMESTAMPTZ` | time types are stored as two columns, one with the `_nanos` suffix with full nanoseconds precision, and another as a `TIMESTAMPTZ` generated column with microsecond precision | | `DurationKind` | `BIGINT` | durations are stored as a single column in nanoseconds | | `EnumKind` | `_` | a custom enum type is created for each module prefixed with the module name it pertains to | From f63bc4a8722864ce06453e0670af9b44e434a3bc Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 9 Jul 2024 13:37:07 +0200 Subject: [PATCH 75/79] improve test coverage, remove go.mod replaces --- indexer/postgres/create_table.go | 2 +- indexer/postgres/create_table_test.go | 19 +++++++++++++------ indexer/postgres/go.mod | 4 +--- indexer/postgres/go.sum | 2 ++ .../internal/testdata/example_schema.go | 4 ++++ indexer/postgres/tests/go.mod | 4 +--- indexer/postgres/tests/go.sum | 2 ++ .../postgres/tests/testdata/init_schema.txt | 13 +++++++++---- .../testdata/init_schema_no_retain_delete.txt | 13 +++++++++---- 9 files changed, 42 insertions(+), 21 deletions(-) diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index b2d1c487e024..d5301a15d130 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -25,7 +25,7 @@ func (tm *TableManager) CreateTable(ctx context.Context, conn DBConn) error { // CreateTableSql generates a CREATE TABLE statement for the object type. func (tm *TableManager) CreateTableSql(writer io.Writer) error { - _, err := fmt.Fprintf(writer, "CREATE TABLE IF NOT EXISTS %q (", tm.TableName()) + _, err := fmt.Fprintf(writer, "CREATE TABLE IF NOT EXISTS %q (\n\t", tm.TableName()) if err != nil { return err } diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go index 1ee663e6ad5b..af85d073405a 100644 --- a/indexer/postgres/create_table_test.go +++ b/indexer/postgres/create_table_test.go @@ -3,14 +3,18 @@ package postgres import ( "os" - "cosmossdk.io/indexer/postgres/internal/testdata" "cosmossdk.io/schema" + + "cosmossdk.io/indexer/postgres/internal/testdata" ) func ExampleTableManager_CreateTableSql_allKinds() { exampleCreateTable(testdata.AllKindsObject) // Output: - // CREATE TABLE IF NOT EXISTS "test_all_kinds" ("id" BIGINT NOT NULL, + // CREATE TABLE IF NOT EXISTS "test_all_kinds" ( + // "id" BIGINT NOT NULL, + // "ts" TIMESTAMPTZ GENERATED ALWAYS AS (nanos_to_timestamptz("ts_nanos")) STORED, + // "ts_nanos" BIGINT NOT NULL, // "string" TEXT NOT NULL, // "bytes" BYTEA NOT NULL, // "int8" SMALLINT NOT NULL, @@ -32,7 +36,7 @@ func ExampleTableManager_CreateTableSql_allKinds() { // "bech32address" TEXT NOT NULL, // "enum" "test_my_enum" NOT NULL, // "json" JSONB NOT NULL, - // PRIMARY KEY ("id") + // PRIMARY KEY ("id", "ts_nanos") // ); // GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; } @@ -40,7 +44,8 @@ func ExampleTableManager_CreateTableSql_allKinds() { func ExampleTableManager_CreateTableSql_singleton() { exampleCreateTable(testdata.SingletonObject) // Output: - // CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), + // CREATE TABLE IF NOT EXISTS "test_singleton" ( + // _id INTEGER NOT NULL CHECK (_id = 1), // "foo" TEXT NOT NULL, // "bar" INTEGER NULL, // "an_enum" "test_my_enum" NOT NULL, @@ -52,7 +57,8 @@ func ExampleTableManager_CreateTableSql_singleton() { func ExampleTableManager_CreateTableSql_vote() { exampleCreateTable(testdata.VoteObject) // Output: - // CREATE TABLE IF NOT EXISTS "test_vote" ("proposal" BIGINT NOT NULL, + // CREATE TABLE IF NOT EXISTS "test_vote" ( + // "proposal" BIGINT NOT NULL, // "address" TEXT NOT NULL, // "vote" "test_vote_type" NOT NULL, // _deleted BOOLEAN NOT NULL DEFAULT FALSE, @@ -64,7 +70,8 @@ func ExampleTableManager_CreateTableSql_vote() { func ExampleTableManager_CreateTableSql_vote_no_retain_delete() { exampleCreateTableOpt(testdata.VoteObject, true) // Output: - // CREATE TABLE IF NOT EXISTS "test_vote" ("proposal" BIGINT NOT NULL, + // CREATE TABLE IF NOT EXISTS "test_vote" ( + // "proposal" BIGINT NOT NULL, // "address" TEXT NOT NULL, // "vote" "test_vote_type" NOT NULL, // PRIMARY KEY ("proposal", "address") diff --git a/indexer/postgres/go.mod b/indexer/postgres/go.mod index acac6324b497..d85dbc46718e 100644 --- a/indexer/postgres/go.mod +++ b/indexer/postgres/go.mod @@ -8,6 +8,4 @@ go 1.12 // so there are no problems building this with any version of the SDK. // This module should only use the golang standard library (database/sql) // and cosmossdk.io/indexer/base. -require cosmossdk.io/schema v0.0.0 - -replace cosmossdk.io/schema => ../../schema +require cosmossdk.io/schema v0.1.1 diff --git a/indexer/postgres/go.sum b/indexer/postgres/go.sum index e69de29bb2d1..6a92c3d3ec66 100644 --- a/indexer/postgres/go.sum +++ b/indexer/postgres/go.sum @@ -0,0 +1,2 @@ +cosmossdk.io/schema v0.1.1 h1:I0M6pgI7R10nq+/HCQfbO6BsGBZA8sQy+duR1Y3aKcA= +cosmossdk.io/schema v0.1.1/go.mod h1:RDAhxIeNB4bYqAlF4NBJwRrgtnciMcyyg0DOKnhNZQQ= diff --git a/indexer/postgres/internal/testdata/example_schema.go b/indexer/postgres/internal/testdata/example_schema.go index 232dd5a3cc3b..ccdd39d96c35 100644 --- a/indexer/postgres/internal/testdata/example_schema.go +++ b/indexer/postgres/internal/testdata/example_schema.go @@ -14,6 +14,10 @@ func init() { Name: "id", Kind: schema.Int64Kind, }, + { + Name: "ts", + Kind: schema.TimeKind, + }, }, } diff --git a/indexer/postgres/tests/go.mod b/indexer/postgres/tests/go.mod index 07a4b6861c10..d5a29304251f 100644 --- a/indexer/postgres/tests/go.mod +++ b/indexer/postgres/tests/go.mod @@ -2,7 +2,7 @@ module cosmossdk.io/indexer/postgres/testing require ( cosmossdk.io/indexer/postgres v0.0.0-00010101000000-000000000000 - cosmossdk.io/schema v0.0.0 + cosmossdk.io/schema v0.1.1 github.com/fergusstrange/embedded-postgres v1.27.0 github.com/hashicorp/consul/sdk v0.16.1 github.com/jackc/pgx/v5 v5.6.0 @@ -28,8 +28,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace cosmossdk.io/schema => ../../../schema - replace cosmossdk.io/indexer/postgres => ../. go 1.22 diff --git a/indexer/postgres/tests/go.sum b/indexer/postgres/tests/go.sum index de3d49d74702..a4ba87b486c2 100644 --- a/indexer/postgres/tests/go.sum +++ b/indexer/postgres/tests/go.sum @@ -1,3 +1,5 @@ +cosmossdk.io/schema v0.1.1 h1:I0M6pgI7R10nq+/HCQfbO6BsGBZA8sQy+duR1Y3aKcA= +cosmossdk.io/schema v0.1.1/go.mod h1:RDAhxIeNB4bYqAlF4NBJwRrgtnciMcyyg0DOKnhNZQQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= diff --git a/indexer/postgres/tests/testdata/init_schema.txt b/indexer/postgres/tests/testdata/init_schema.txt index f094cdb5523b..e2a0a1730e35 100644 --- a/indexer/postgres/tests/testdata/init_schema.txt +++ b/indexer/postgres/tests/testdata/init_schema.txt @@ -5,7 +5,10 @@ Creating enum type CREATE TYPE "test_vote_type" AS ENUM ('yes', 'no', 'abstain'); Creating table test_all_kinds -CREATE TABLE IF NOT EXISTS "test_all_kinds" ("id" BIGINT NOT NULL, +CREATE TABLE IF NOT EXISTS "test_all_kinds" ( + "id" BIGINT NOT NULL, + "ts" TIMESTAMPTZ GENERATED ALWAYS AS (nanos_to_timestamptz("ts_nanos")) STORED, + "ts_nanos" BIGINT NOT NULL, "string" TEXT NOT NULL, "bytes" BYTEA NOT NULL, "int8" SMALLINT NOT NULL, @@ -27,12 +30,13 @@ CREATE TABLE IF NOT EXISTS "test_all_kinds" ("id" BIGINT NOT NULL, "bech32address" TEXT NOT NULL, "enum" "test_my_enum" NOT NULL, "json" JSONB NOT NULL, - PRIMARY KEY ("id") + PRIMARY KEY ("id", "ts_nanos") ); GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; Creating table test_singleton -CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), +CREATE TABLE IF NOT EXISTS "test_singleton" ( + _id INTEGER NOT NULL CHECK (_id = 1), "foo" TEXT NOT NULL, "bar" INTEGER NULL, "an_enum" "test_my_enum" NOT NULL, @@ -41,7 +45,8 @@ CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1 GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; Creating table test_vote -CREATE TABLE IF NOT EXISTS "test_vote" ("proposal" BIGINT NOT NULL, +CREATE TABLE IF NOT EXISTS "test_vote" ( + "proposal" BIGINT NOT NULL, "address" TEXT NOT NULL, "vote" "test_vote_type" NOT NULL, _deleted BOOLEAN NOT NULL DEFAULT FALSE, diff --git a/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt b/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt index 341e11e67501..0d8cdad2cd24 100644 --- a/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt +++ b/indexer/postgres/tests/testdata/init_schema_no_retain_delete.txt @@ -5,7 +5,10 @@ Creating enum type CREATE TYPE "test_vote_type" AS ENUM ('yes', 'no', 'abstain'); Creating table test_all_kinds -CREATE TABLE IF NOT EXISTS "test_all_kinds" ("id" BIGINT NOT NULL, +CREATE TABLE IF NOT EXISTS "test_all_kinds" ( + "id" BIGINT NOT NULL, + "ts" TIMESTAMPTZ GENERATED ALWAYS AS (nanos_to_timestamptz("ts_nanos")) STORED, + "ts_nanos" BIGINT NOT NULL, "string" TEXT NOT NULL, "bytes" BYTEA NOT NULL, "int8" SMALLINT NOT NULL, @@ -27,12 +30,13 @@ CREATE TABLE IF NOT EXISTS "test_all_kinds" ("id" BIGINT NOT NULL, "bech32address" TEXT NOT NULL, "enum" "test_my_enum" NOT NULL, "json" JSONB NOT NULL, - PRIMARY KEY ("id") + PRIMARY KEY ("id", "ts_nanos") ); GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; Creating table test_singleton -CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1), +CREATE TABLE IF NOT EXISTS "test_singleton" ( + _id INTEGER NOT NULL CHECK (_id = 1), "foo" TEXT NOT NULL, "bar" INTEGER NULL, "an_enum" "test_my_enum" NOT NULL, @@ -41,7 +45,8 @@ CREATE TABLE IF NOT EXISTS "test_singleton" (_id INTEGER NOT NULL CHECK (_id = 1 GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; Creating table test_vote -CREATE TABLE IF NOT EXISTS "test_vote" ("proposal" BIGINT NOT NULL, +CREATE TABLE IF NOT EXISTS "test_vote" ( + "proposal" BIGINT NOT NULL, "address" TEXT NOT NULL, "vote" "test_vote_type" NOT NULL, PRIMARY KEY ("proposal", "address") From 9e960126655e933118b9a9e01861846ee8993415 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 10 Jul 2024 13:25:40 +0200 Subject: [PATCH 76/79] lint fix --- indexer/postgres/create_table_test.go | 3 +-- indexer/postgres/tests/init_schema_test.go | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go index af85d073405a..33f4d513eda6 100644 --- a/indexer/postgres/create_table_test.go +++ b/indexer/postgres/create_table_test.go @@ -3,9 +3,8 @@ package postgres import ( "os" - "cosmossdk.io/schema" - "cosmossdk.io/indexer/postgres/internal/testdata" + "cosmossdk.io/schema" ) func ExampleTableManager_CreateTableSql_allKinds() { diff --git a/indexer/postgres/tests/init_schema_test.go b/indexer/postgres/tests/init_schema_test.go index c77fa7fecd78..e9f00a78dc28 100644 --- a/indexer/postgres/tests/init_schema_test.go +++ b/indexer/postgres/tests/init_schema_test.go @@ -9,12 +9,10 @@ import ( embeddedpostgres "github.com/fergusstrange/embedded-postgres" "github.com/hashicorp/consul/sdk/freeport" - "gotest.tools/v3/golden" - // this is where we get our pgx database driver from _ "github.com/jackc/pgx/v5/stdlib" - "github.com/stretchr/testify/require" + "gotest.tools/v3/golden" "cosmossdk.io/indexer/postgres" "cosmossdk.io/indexer/postgres/internal/testdata" From da3203cb8216bbef24e3132ffd41429a7373a55d Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Sun, 14 Jul 2024 12:07:42 +0200 Subject: [PATCH 77/79] rename managers to indexers --- indexer/postgres/column.go | 4 ++-- indexer/postgres/create_table.go | 4 ++-- indexer/postgres/create_table_test.go | 13 +++++----- indexer/postgres/enum.go | 4 ++-- indexer/postgres/indexer.go | 6 ++--- indexer/postgres/{module_mgr.go => module.go} | 24 +++++++++---------- indexer/postgres/{table_mgr.go => object.go} | 14 +++++------ indexer/postgres/options.go | 4 ++-- 8 files changed, 37 insertions(+), 36 deletions(-) rename indexer/postgres/{module_mgr.go => module.go} (62%) rename indexer/postgres/{table_mgr.go => object.go} (66%) diff --git a/indexer/postgres/column.go b/indexer/postgres/column.go index b5a2b47613c1..f9692af13711 100644 --- a/indexer/postgres/column.go +++ b/indexer/postgres/column.go @@ -8,7 +8,7 @@ import ( ) // createColumnDefinition writes a column definition within a CREATE TABLE statement for the field. -func (tm *TableManager) createColumnDefinition(writer io.Writer, field schema.Field) error { +func (tm *ObjectIndexer) createColumnDefinition(writer io.Writer, field schema.Field) error { _, err := fmt.Fprintf(writer, "%q ", field.Name) if err != nil { return err @@ -110,7 +110,7 @@ func simpleColumnType(kind schema.Kind) string { // updatableColumnName is the name of the insertable/updatable column name for the field. // This is the field name in most cases, except for time columns which are stored as nanos // and then converted to timestamp generated columns. -func (tm *TableManager) updatableColumnName(field schema.Field) (name string, err error) { +func (tm *ObjectIndexer) updatableColumnName(field schema.Field) (name string, err error) { name = field.Name if field.Kind == schema.TimeKind { name = fmt.Sprintf("%s_nanos", name) diff --git a/indexer/postgres/create_table.go b/indexer/postgres/create_table.go index d5301a15d130..8f5f0e6ca206 100644 --- a/indexer/postgres/create_table.go +++ b/indexer/postgres/create_table.go @@ -8,7 +8,7 @@ import ( ) // CreateTable creates the table for the object type. -func (tm *TableManager) CreateTable(ctx context.Context, conn DBConn) error { +func (tm *ObjectIndexer) CreateTable(ctx context.Context, conn DBConn) error { buf := new(strings.Builder) err := tm.CreateTableSql(buf) if err != nil { @@ -24,7 +24,7 @@ func (tm *TableManager) CreateTable(ctx context.Context, conn DBConn) error { } // CreateTableSql generates a CREATE TABLE statement for the object type. -func (tm *TableManager) CreateTableSql(writer io.Writer) error { +func (tm *ObjectIndexer) CreateTableSql(writer io.Writer) error { _, err := fmt.Fprintf(writer, "CREATE TABLE IF NOT EXISTS %q (\n\t", tm.TableName()) if err != nil { return err diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go index 33f4d513eda6..8b0d7a129b0e 100644 --- a/indexer/postgres/create_table_test.go +++ b/indexer/postgres/create_table_test.go @@ -3,11 +3,12 @@ package postgres import ( "os" - "cosmossdk.io/indexer/postgres/internal/testdata" "cosmossdk.io/schema" + + "cosmossdk.io/indexer/postgres/internal/testdata" ) -func ExampleTableManager_CreateTableSql_allKinds() { +func ExampleObjectIndexer_CreateTableSql_allKinds() { exampleCreateTable(testdata.AllKindsObject) // Output: // CREATE TABLE IF NOT EXISTS "test_all_kinds" ( @@ -40,7 +41,7 @@ func ExampleTableManager_CreateTableSql_allKinds() { // GRANT SELECT ON TABLE "test_all_kinds" TO PUBLIC; } -func ExampleTableManager_CreateTableSql_singleton() { +func ExampleObjectIndexer_CreateTableSql_singleton() { exampleCreateTable(testdata.SingletonObject) // Output: // CREATE TABLE IF NOT EXISTS "test_singleton" ( @@ -53,7 +54,7 @@ func ExampleTableManager_CreateTableSql_singleton() { // GRANT SELECT ON TABLE "test_singleton" TO PUBLIC; } -func ExampleTableManager_CreateTableSql_vote() { +func ExampleObjectIndexer_CreateTableSql_vote() { exampleCreateTable(testdata.VoteObject) // Output: // CREATE TABLE IF NOT EXISTS "test_vote" ( @@ -66,7 +67,7 @@ func ExampleTableManager_CreateTableSql_vote() { // GRANT SELECT ON TABLE "test_vote" TO PUBLIC; } -func ExampleTableManager_CreateTableSql_vote_no_retain_delete() { +func ExampleObjectIndexer_CreateTableSql_vote_no_retain_delete() { exampleCreateTableOpt(testdata.VoteObject, true) // Output: // CREATE TABLE IF NOT EXISTS "test_vote" ( @@ -83,7 +84,7 @@ func exampleCreateTable(objectType schema.ObjectType) { } func exampleCreateTableOpt(objectType schema.ObjectType, noRetainDelete bool) { - tm := NewTableManager("test", objectType, ManagerOptions{ + tm := NewObjectIndexer("test", objectType, Options{ Logger: func(msg, sql string, params ...interface{}) {}, DisableRetainDeletions: noRetainDelete, }) diff --git a/indexer/postgres/enum.go b/indexer/postgres/enum.go index eb108c2db796..c438257d2020 100644 --- a/indexer/postgres/enum.go +++ b/indexer/postgres/enum.go @@ -11,7 +11,7 @@ import ( ) // CreateEnumType creates an enum type in the database. -func (m *ModuleManager) CreateEnumType(ctx context.Context, conn DBConn, enum schema.EnumDefinition) error { +func (m *ModuleIndexer) CreateEnumType(ctx context.Context, conn DBConn, enum schema.EnumDefinition) error { typeName := enumTypeName(m.moduleName, enum) row := conn.QueryRowContext(ctx, "SELECT 1 FROM pg_type WHERE typname = $1", typeName) var res interface{} @@ -68,7 +68,7 @@ func enumTypeName(moduleName string, enum schema.EnumDefinition) string { } // createEnumTypesForFields creates enum types for all the fields that have enum kind in the module schema. -func (m *ModuleManager) createEnumTypesForFields(ctx context.Context, conn DBConn, fields []schema.Field) error { +func (m *ModuleIndexer) createEnumTypesForFields(ctx context.Context, conn DBConn, fields []schema.Field) error { for _, field := range fields { if field.Kind != schema.EnumKind { continue diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index 60951bec10c3..3aac43bb943d 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -47,8 +47,8 @@ func StartIndexer(ctx context.Context, logger SqlLogger, config Config) (appdata return appdata.Listener{}, err } - moduleMgrs := map[string]*ModuleManager{} - mgrOpts := ManagerOptions{ + moduleMgrs := map[string]*ModuleIndexer{} + mgrOpts := Options{ DisableRetainDeletions: config.DisableRetainDeletions, Logger: logger, } @@ -62,7 +62,7 @@ func StartIndexer(ctx context.Context, logger SqlLogger, config Config) (appdata return fmt.Errorf("module %s already initialized", moduleName) } - mm := NewModuleManager(moduleName, modSchema, mgrOpts) + mm := NewModuleIndexer(moduleName, modSchema, mgrOpts) moduleMgrs[moduleName] = mm return mm.InitializeSchema(ctx, tx) diff --git a/indexer/postgres/module_mgr.go b/indexer/postgres/module.go similarity index 62% rename from indexer/postgres/module_mgr.go rename to indexer/postgres/module.go index 09429a1540bf..57564700b78a 100644 --- a/indexer/postgres/module_mgr.go +++ b/indexer/postgres/module.go @@ -7,28 +7,28 @@ import ( "cosmossdk.io/schema" ) -// ModuleManager manages the tables for a module. -type ModuleManager struct { +// ModuleIndexer manages the tables for a module. +type ModuleIndexer struct { moduleName string schema schema.ModuleSchema - tables map[string]*TableManager + tables map[string]*ObjectIndexer definedEnums map[string]schema.EnumDefinition - options ManagerOptions + options Options } -// NewModuleManager creates a new ModuleManager for the given module schema. -func NewModuleManager(moduleName string, modSchema schema.ModuleSchema, options ManagerOptions) *ModuleManager { - return &ModuleManager{ +// NewModuleIndexer creates a new ModuleIndexer for the given module schema. +func NewModuleIndexer(moduleName string, modSchema schema.ModuleSchema, options Options) *ModuleIndexer { + return &ModuleIndexer{ moduleName: moduleName, schema: modSchema, - tables: map[string]*TableManager{}, + tables: map[string]*ObjectIndexer{}, definedEnums: map[string]schema.EnumDefinition{}, options: options, } } // InitializeSchema creates tables for all object types in the module schema and creates enum types. -func (m *ModuleManager) InitializeSchema(ctx context.Context, conn DBConn) error { +func (m *ModuleIndexer) InitializeSchema(ctx context.Context, conn DBConn) error { // create enum types for _, typ := range m.schema.ObjectTypes { err := m.createEnumTypesForFields(ctx, conn, typ.KeyFields) @@ -44,7 +44,7 @@ func (m *ModuleManager) InitializeSchema(ctx context.Context, conn DBConn) error // create tables for all object types for _, typ := range m.schema.ObjectTypes { - tm := NewTableManager(m.moduleName, typ, m.options) + tm := NewObjectIndexer(m.moduleName, typ, m.options) m.tables[typ.Name] = tm err := tm.CreateTable(ctx, conn) if err != nil { @@ -55,7 +55,7 @@ func (m *ModuleManager) InitializeSchema(ctx context.Context, conn DBConn) error return nil } -// Tables returns the table managers for the module. -func (m *ModuleManager) Tables() map[string]*TableManager { +// ObjectIndexers returns the object indexers for the module. +func (m *ModuleIndexer) ObjectIndexers() map[string]*ObjectIndexer { return m.tables } diff --git a/indexer/postgres/table_mgr.go b/indexer/postgres/object.go similarity index 66% rename from indexer/postgres/table_mgr.go rename to indexer/postgres/object.go index 5236514bc930..78bbfdf636b1 100644 --- a/indexer/postgres/table_mgr.go +++ b/indexer/postgres/object.go @@ -6,17 +6,17 @@ import ( "cosmossdk.io/schema" ) -// TableManager is a helper struct that generates SQL for a given object type. -type TableManager struct { +// ObjectIndexer is a helper struct that generates SQL for a given object type. +type ObjectIndexer struct { moduleName string typ schema.ObjectType valueFields map[string]schema.Field allFields map[string]schema.Field - options ManagerOptions + options Options } -// NewTableManager creates a new TableManager for the given object type. -func NewTableManager(moduleName string, typ schema.ObjectType, options ManagerOptions) *TableManager { +// NewObjectIndexer creates a new ObjectIndexer for the given object type. +func NewObjectIndexer(moduleName string, typ schema.ObjectType, options Options) *ObjectIndexer { allFields := make(map[string]schema.Field) valueFields := make(map[string]schema.Field) @@ -29,7 +29,7 @@ func NewTableManager(moduleName string, typ schema.ObjectType, options ManagerOp allFields[field.Name] = field } - return &TableManager{ + return &ObjectIndexer{ moduleName: moduleName, typ: typ, allFields: allFields, @@ -39,6 +39,6 @@ func NewTableManager(moduleName string, typ schema.ObjectType, options ManagerOp } // TableName returns the name of the table for the object type scoped to its module. -func (tm *TableManager) TableName() string { +func (tm *ObjectIndexer) TableName() string { return fmt.Sprintf("%s_%s", tm.moduleName, tm.typ.Name) } diff --git a/indexer/postgres/options.go b/indexer/postgres/options.go index 9d4514a6e713..be93d43b6c76 100644 --- a/indexer/postgres/options.go +++ b/indexer/postgres/options.go @@ -1,7 +1,7 @@ package postgres -// ManagerOptions are the options for module and table managers. -type ManagerOptions struct { +// Options are the options for module and object indexers. +type Options struct { // DisableRetainDeletions disables retain deletions functionality even on object types that have it set. DisableRetainDeletions bool From 16cf73424c4c45807ab211aa0cdd6f5cdf8fd70c Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Sun, 14 Jul 2024 12:08:29 +0200 Subject: [PATCH 78/79] rename --- indexer/postgres/indexer.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/indexer/postgres/indexer.go b/indexer/postgres/indexer.go index 3aac43bb943d..afcd8e0d8dbf 100644 --- a/indexer/postgres/indexer.go +++ b/indexer/postgres/indexer.go @@ -47,8 +47,8 @@ func StartIndexer(ctx context.Context, logger SqlLogger, config Config) (appdata return appdata.Listener{}, err } - moduleMgrs := map[string]*ModuleIndexer{} - mgrOpts := Options{ + moduleIndexers := map[string]*ModuleIndexer{} + opts := Options{ DisableRetainDeletions: config.DisableRetainDeletions, Logger: logger, } @@ -57,13 +57,13 @@ func StartIndexer(ctx context.Context, logger SqlLogger, config Config) (appdata InitializeModuleData: func(data appdata.ModuleInitializationData) error { moduleName := data.ModuleName modSchema := data.Schema - _, ok := moduleMgrs[moduleName] + _, ok := moduleIndexers[moduleName] if ok { return fmt.Errorf("module %s already initialized", moduleName) } - mm := NewModuleIndexer(moduleName, modSchema, mgrOpts) - moduleMgrs[moduleName] = mm + mm := NewModuleIndexer(moduleName, modSchema, opts) + moduleIndexers[moduleName] = mm return mm.InitializeSchema(ctx, tx) }, From 191eee1673bbdaa246d54d47c05d373c8a61bc00 Mon Sep 17 00:00:00 2001 From: marbar3778 Date: Thu, 18 Jul 2024 11:18:41 +0200 Subject: [PATCH 79/79] lint --- indexer/postgres/create_table_test.go | 3 +-- indexer/postgres/tests/init_schema_test.go | 3 +++ schema/field.go | 6 +++--- schema/object_type.go | 6 +++--- .../defaults/lockup/continuous_locking_account_test.go | 3 ++- x/accounts/defaults/lockup/delayed_locking_account_test.go | 3 ++- x/accounts/defaults/lockup/periodic_locking_account_test.go | 3 ++- .../defaults/lockup/permanent_locking_account_test.go | 3 ++- 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/indexer/postgres/create_table_test.go b/indexer/postgres/create_table_test.go index 8b0d7a129b0e..dec09d7aed7a 100644 --- a/indexer/postgres/create_table_test.go +++ b/indexer/postgres/create_table_test.go @@ -3,9 +3,8 @@ package postgres import ( "os" - "cosmossdk.io/schema" - "cosmossdk.io/indexer/postgres/internal/testdata" + "cosmossdk.io/schema" ) func ExampleObjectIndexer_CreateTableSql_allKinds() { diff --git a/indexer/postgres/tests/init_schema_test.go b/indexer/postgres/tests/init_schema_test.go index e9f00a78dc28..1afa6caea9b4 100644 --- a/indexer/postgres/tests/init_schema_test.go +++ b/indexer/postgres/tests/init_schema_test.go @@ -9,6 +9,7 @@ import ( embeddedpostgres "github.com/fergusstrange/embedded-postgres" "github.com/hashicorp/consul/sdk/freeport" + // this is where we get our pgx database driver from _ "github.com/jackc/pgx/v5/stdlib" "github.com/stretchr/testify/require" @@ -30,6 +31,7 @@ func TestInitSchema(t *testing.T) { } func testInitSchema(t *testing.T, disableRetainDeletions bool, goldenFileName string) { + t.Helper() connectionUrl := createTestDB(t) buf := &strings.Builder{} @@ -64,6 +66,7 @@ func testInitSchema(t *testing.T, disableRetainDeletions bool, goldenFileName st } func createTestDB(t *testing.T) (connectionUrl string) { + t.Helper() tempDir, err := os.MkdirTemp("", "postgres-indexer-test") require.NoError(t, err) t.Cleanup(func() { diff --git a/schema/field.go b/schema/field.go index 6a5df03a10ec..19a1b4085dd6 100644 --- a/schema/field.go +++ b/schema/field.go @@ -33,7 +33,7 @@ func (c Field) Validate() error { // valid kind if err := c.Kind.Validate(); err != nil { - return fmt.Errorf("invalid field kind for %q: %v", c.Name, err) + return fmt.Errorf("invalid field kind for %q: %v", c.Name, err) //nolint:errorlint // false positive due to using go1.12 } // address prefix only valid with Bech32AddressKind @@ -46,7 +46,7 @@ func (c Field) Validate() error { // enum definition only valid with EnumKind if c.Kind == EnumKind { if err := c.EnumDefinition.Validate(); err != nil { - return fmt.Errorf("invalid enum definition for field %q: %v", c.Name, err) + return fmt.Errorf("invalid enum definition for field %q: %v", c.Name, err) //nolint:errorlint // false positive due to using go1.12 } } else if c.Kind != EnumKind && (c.EnumDefinition.Name != "" || c.EnumDefinition.Values != nil) { return fmt.Errorf("enum definition is only valid for field %q with type EnumKind", c.Name) @@ -67,7 +67,7 @@ func (c Field) ValidateValue(value interface{}) error { } err := c.Kind.ValidateValueType(value) if err != nil { - return fmt.Errorf("invalid value for field %q: %v", c.Name, err) + return fmt.Errorf("invalid value for field %q: %v", c.Name, err) //nolint:errorlint // false positive due to using go1.12 } if c.Kind == EnumKind { diff --git a/schema/object_type.go b/schema/object_type.go index 4189f88de82d..a8fa432d8032 100644 --- a/schema/object_type.go +++ b/schema/object_type.go @@ -43,7 +43,7 @@ func (o ObjectType) validate(enumValueMap map[string]map[string]bool) error { for _, field := range o.KeyFields { if err := field.Validate(); err != nil { - return fmt.Errorf("invalid key field %q: %v", field.Name, err) + return fmt.Errorf("invalid key field %q: %v", field.Name, err) //nolint:errorlint // false positive due to using go1.12 } if field.Nullable { @@ -62,7 +62,7 @@ func (o ObjectType) validate(enumValueMap map[string]map[string]bool) error { for _, field := range o.ValueFields { if err := field.Validate(); err != nil { - return fmt.Errorf("invalid value field %q: %v", field.Name, err) + return fmt.Errorf("invalid value field %q: %v", field.Name, err) //nolint:errorlint // false positive due to using go1.12 } if fieldNames[field.Name] { @@ -89,7 +89,7 @@ func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error { } if err := ValidateObjectKey(o.KeyFields, update.Key); err != nil { - return fmt.Errorf("invalid key for object type %q: %v", update.TypeName, err) + return fmt.Errorf("invalid key for object type %q: %v", update.TypeName, err) //nolint:errorlint // false positive due to using go1.12 } if update.Delete { diff --git a/x/accounts/defaults/lockup/continuous_locking_account_test.go b/x/accounts/defaults/lockup/continuous_locking_account_test.go index 1c5742833a81..dd208b26be74 100644 --- a/x/accounts/defaults/lockup/continuous_locking_account_test.go +++ b/x/accounts/defaults/lockup/continuous_locking_account_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "cosmossdk.io/core/header" @@ -13,6 +12,8 @@ import ( "cosmossdk.io/log" "cosmossdk.io/math" lockuptypes "cosmossdk.io/x/accounts/defaults/lockup/types" + + sdk "github.com/cosmos/cosmos-sdk/types" ) func setupContinousAccount(t *testing.T, ctx context.Context, ss store.KVStoreService) *ContinuousLockingAccount { diff --git a/x/accounts/defaults/lockup/delayed_locking_account_test.go b/x/accounts/defaults/lockup/delayed_locking_account_test.go index 2a9fa85b755c..9a5f2cc4ae2f 100644 --- a/x/accounts/defaults/lockup/delayed_locking_account_test.go +++ b/x/accounts/defaults/lockup/delayed_locking_account_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "cosmossdk.io/core/header" @@ -13,6 +12,8 @@ import ( "cosmossdk.io/log" "cosmossdk.io/math" lockuptypes "cosmossdk.io/x/accounts/defaults/lockup/types" + + sdk "github.com/cosmos/cosmos-sdk/types" ) func setupDelayedAccount(t *testing.T, ctx context.Context, ss store.KVStoreService) *DelayedLockingAccount { diff --git a/x/accounts/defaults/lockup/periodic_locking_account_test.go b/x/accounts/defaults/lockup/periodic_locking_account_test.go index e05b033430be..809fe33757d0 100644 --- a/x/accounts/defaults/lockup/periodic_locking_account_test.go +++ b/x/accounts/defaults/lockup/periodic_locking_account_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "cosmossdk.io/core/header" @@ -13,6 +12,8 @@ import ( "cosmossdk.io/log" "cosmossdk.io/math" lockuptypes "cosmossdk.io/x/accounts/defaults/lockup/types" + + sdk "github.com/cosmos/cosmos-sdk/types" ) func setupPeriodicAccount(t *testing.T, ctx context.Context, ss store.KVStoreService) *PeriodicLockingAccount { diff --git a/x/accounts/defaults/lockup/permanent_locking_account_test.go b/x/accounts/defaults/lockup/permanent_locking_account_test.go index 341cd3c681ca..057bc95689c0 100644 --- a/x/accounts/defaults/lockup/permanent_locking_account_test.go +++ b/x/accounts/defaults/lockup/permanent_locking_account_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" "cosmossdk.io/core/header" @@ -13,6 +12,8 @@ import ( "cosmossdk.io/log" "cosmossdk.io/math" lockuptypes "cosmossdk.io/x/accounts/defaults/lockup/types" + + sdk "github.com/cosmos/cosmos-sdk/types" ) func setupPermanentAccount(t *testing.T, ctx context.Context, ss store.KVStoreService) *PermanentLockingAccount {