feat: add platform-collection relationship in platform.FetchCollection()

This commit is contained in:
Alexander Navarro 2024-11-25 19:54:41 -03:00
parent 8af94161ab
commit c1684c8dea
7 changed files with 184 additions and 79 deletions

View file

@ -134,7 +134,7 @@ func main() {
defer connection.Close()
opts := synchronizator.DefaultOptions
// opts.Log_level = synchronizator.DEBUG
opts.Log_level = synchronizator.DEBUG
opts.DANGEROUSLY_DROP_TABLES = true
sync, err := synchronizator.New(connection, opts)

View file

@ -10,9 +10,13 @@ type default_collection struct {
platform_name string
}
func (collection *default_collection) ToNode() (string, []byte, error) {
func (collection *default_collection) GetClass() string {
platform_name := strings.ToUpper(collection.platform_name)
return platform_name + "_DEFAULT", nil, nil
return platform_name + "_DEFAULT"
}
func (collection *default_collection) GetMetadata() []byte {
return nil
}
func (collection *default_collection) FromNode(_class string, name string, metadata []byte) error {
@ -46,13 +50,19 @@ func NewCollection(name string, metadata []byte) *Collection {
}
// Internal RelationshipClass to handle the collection to node relationship
type collection_relation struct{}
func (collection *collection_relation) ToRelationship() (string, []byte, error) {
return "COLLECTION_HAS", nil, nil
type collection_has_node struct {
Relationship
}
func (collection *collection_relation) FromRelationship(_class string, metadata []byte) error {
func (col_has_node *collection_has_node) GetClass() string {
return "COLLECTION_HAS"
}
func (col_has_node *collection_has_node) GetMetadata() []byte {
return nil
}
func (col_has_node *collection_has_node) FromRelationship(_class string, metadata []byte) error {
if _class != "COLLECTION_HAS" {
return fmt.Errorf("invalid class %s", _class)
}
@ -62,7 +72,7 @@ func (collection *collection_relation) FromRelationship(_class string, metadata
// Adds a new child to this collection. Use the underlaying node's AddRelation
// method.
func (collection *Collection) AddChild(node *Node) error {
_, err := collection.AddRelation(&collection_relation{}, node.Id)
_, err := collection.StoreRelation(&collection_has_node{}, node.Id)
if err != nil {
return err
}

17
pkg/fetcher.go Normal file
View file

@ -0,0 +1,17 @@
package synchronizator
type Fetcher = func(pagination Pagination) ([]*Collection, Pagination, error)
type Pagination struct {
Total int
HasMore bool
Limit int
Offset int
}
var StartPagination = Pagination{
Total: 0,
HasMore: false,
Limit: 10,
Offset: 0,
}

View file

@ -1,10 +1,18 @@
package synchronizator
import (
"fmt"
"slices"
)
type StandardNode interface {
GetClass() string
GetName() string
GetMetadata() []byte
AddRelationship(relationship StandardRelationship) error
AddRelationships(to int64, relationship []StandardRelationship)
SetId(int64)
SetConnection(*DB)
}
@ -14,7 +22,7 @@ type StandardNode interface {
type Node struct {
_conn *DB // Underlaying connection to the database.
_class string // The class of the node, should not be modified to avoid inconsistencies.
_relationships []*Relationship // Relationships of the node
_relationships []StandardRelationship // Relationships of the node
Id int64 // The id of the node
name string // The name of the node
metadata []byte // Arbitrary data. This is stored as a jsonb in the database
@ -23,7 +31,7 @@ type Node struct {
func NewNode(name string, metadata []byte) *Node {
return &Node{
name: name,
_class: "COLLECTION",
_class: "NODE",
metadata: metadata,
Id: -1, // Use -1 to indicate not persisted
}
@ -41,6 +49,26 @@ func (node *Node) GetMetadata() []byte {
return node.metadata
}
func (node *Node) AddRelationship(relationship StandardRelationship) error {
node1, node2 := relationship.GetNodes()
if node1 != node.Id && node2 != node.Id {
return fmt.Errorf(
"The current node (%v) is neither used as the source (%v), nor the destination (%v)",
node.Id,
node1,
node2,
)
}
node._relationships = append(node._relationships, relationship)
return nil
}
func (node *Node) AddRelationships(to int64, relationships []StandardRelationship) {
node._relationships = slices.Concat(node._relationships, relationships)
}
func (node *Node) SetId(id int64) {
node.Id = id
}
@ -53,7 +81,7 @@ func (node *Node) SetConnection(conn *DB) {
// provided id. An error is returned if the relation already exists.
//
// This method is a wrapper around the AddRelation method of the connection.
func (node *Node) AddRelation(relation StandardRelationship, to int64) (*Relationship, error) {
func (node *Node) StoreRelation(relation StandardRelationship, to int64) (*Relationship, error) {
return node._conn.AddRelation(node.Id, relation, to)
}
@ -74,7 +102,7 @@ func (node *Node) DeleteRelation(to int64) error {
// Fetch all the outgoing relations for this node. This method will return a
// slice of relationships pointers and also store them in the node for further
// use.
func (node *Node) GetOutRelations() ([]*Relationship, error) {
func (node *Node) GetOutRelations() ([]StandardRelationship, error) {
sql := `
WITH RECURSIVE NodeRelationships AS (
SELECT
@ -103,13 +131,13 @@ FROM
defer rows.Close()
node._relationships = make([]*Relationship, 0)
node._relationships = make([]StandardRelationship, 0)
for rows.Next() {
relationship := &Relationship{
_conn: node._conn,
}
if err := rows.Scan(&relationship.From, &relationship._class, &relationship.Metadata, &relationship.To); err != nil {
if err := rows.Scan(&relationship.From, &relationship._class, relationship.GetMetadata(), &relationship.To); err != nil {
return nil, err
}
@ -124,8 +152,3 @@ FROM
func (node *Node) Delete() error {
return node._conn.DeleteNode(node.Id)
}
// Returns the class of the node
func (relationship *Relationship) GetClass() string {
return relationship._class
}

View file

@ -2,23 +2,6 @@ package synchronizator
import "slices"
type PlatformClass interface {
// How to transform the struct into a node. It needs to return the class,
// name and a []byte representation of the metadata.
//
// - name: A user friendly name
// - metadata: Arbitrary data. This will be stored as a jsonb in the database
//
ToNode() (string, []byte, error)
// How to transform a node into the struct. This method should modify the
// struct directly as it receives a pointer.
//
// - name: A user friendly name
// - metadata: Arbitrary data. This is stored as a jsonb in the database
FromNode(string, []byte) error
}
// Utility struct to represent a collection of nodes, it's a [Node] itself so all
// the node's functionality is available.
type Platform struct {
@ -26,22 +9,6 @@ type Platform struct {
Collections []*Collection // Child nodes
}
type Fetcher = func(pagination Pagination) ([]*Collection, Pagination, error)
type Pagination struct {
Total int
HasMore bool
Limit int
Offset int
}
var StartPagination = Pagination{
Total: 0,
HasMore: false,
Limit: 10,
Offset: 0,
}
func (platform *Platform) FetchCollections(fetcher Fetcher, start_pagination Pagination) error {
collections, pagination, err := fetcher(start_pagination)
if err != nil {
@ -54,6 +21,26 @@ func (platform *Platform) FetchCollections(fetcher Fetcher, start_pagination Pag
}
err = BulkCreateNode(platform._conn, platform.Collections)
if err != nil {
return err
}
for _, item := range platform.Collections {
err := platform.AddRelationship(
&Relationship{
_class: "PLATFORM_HAS_COLLECTION",
From: platform.Id,
To: item.Id,
})
if err != nil {
return err
}
}
err = BulkCreateRelationships(platform._conn, platform._relationships)
if err != nil {
return err
}
return nil
}

View file

@ -19,20 +19,11 @@ package synchronizator
// return nil
// }
type StandardRelationship interface {
// How to transform the struct into a collection. It needs to return the class,
// and a []byte representation of the metadata.
//
// - class: Is used for classification and query pourposes. It's recomended to provide a constante string to increase consistency.
// - metadata: Arbitrary data. This will be stored as a jsonb in the database
//
ToRelationship() (string, []byte, error)
GetClass() string
GetNodes() (int64, int64)
GetMetadata() []byte
// How to transform a relationship into the struct. This method should modify the
// struct directly as it receives a pointer.
//
// - class: Is used for classification and query pourposes.
// - metadata: Arbitrary data. This is stored as a jsonb in the database
FromRelationship(string, []byte) error
SetConnection(*DB)
}
// A relationship in the database.
@ -42,5 +33,21 @@ type Relationship struct {
_class string // The class of the node, should not be modified to avoid inconsistencies.
From int64 // From what node this relation comes from
To int64 // To what node this relation goes to
Metadata []byte // Arbitrary data. This is stored as a jsonb in the database
metadata []byte // Arbitrary data. This is stored as a jsonb in the database
}
func (relation *Relationship) GetClass() string {
return relation._class
}
func (relation *Relationship) GetMetadata() []byte {
return relation.metadata
}
func (relation *Relationship) GetNodes() (int64, int64) {
return relation.From, relation.To
}
func (relation *Relationship) SetConnection(db *DB) {
relation._conn = db
}

View file

@ -193,7 +193,7 @@ func (conn *DB) NewPlatform(name string, metadata []byte) (*Platform, error) {
Collections: []*Collection{collection},
}
_, err = conn.addRelationwithTx(tx, platform.Id, &collection_relation{}, collection.Id)
_, err = conn.addRelationwithTx(tx, platform.Id, &collection_has_node{}, collection.Id)
if err != nil {
return err
}
@ -381,15 +381,13 @@ func (conn *DB) addRelationwithTx(
data StandardRelationship,
to int64,
) (*Relationship, error) {
class, metadata, err := data.ToRelationship()
if err != nil {
return nil, err
}
class := data.GetClass()
metadata := data.GetMetadata()
relationship := Relationship{
_conn: conn,
_class: class,
Metadata: metadata,
metadata: metadata,
From: from,
To: to,
}
@ -398,7 +396,7 @@ func (conn *DB) addRelationwithTx(
sql := "INSERT INTO relationships (_class, node_from, node_to, metadata) VALUES ($1, $2, $3, $4) RETURNING node_from, node_to;"
_, err = tx.Exec(sql, relationship._class, relationship.From, relationship.To, metadata)
_, err := tx.Exec(sql, relationship._class, relationship.From, relationship.To, metadata)
if err != nil {
return nil, err
}
@ -518,3 +516,66 @@ func BulkCreateNode[T StandardNode](
return rows.Err()
}
func BulkCreateRelationships[T StandardRelationship](
conn *DB,
relationships []T,
) error {
if len(relationships) == 0 {
return nil
}
tx, err := conn.Connection.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Build the query dynamically based on number of nodes
valueStrings := make([]string, 0, len(relationships))
valueArgs := make([]interface{}, 0, len(relationships)*3)
for i := range relationships {
n := i * 4
valueStrings = append(
valueStrings,
fmt.Sprintf("($%d, $%d, $%d, $%d) ", n+1, n+2, n+3, n+4),
)
node1, node2 := relationships[i].GetNodes()
class := relationships[i].GetClass()
metadata := relationships[i].GetMetadata()
relationships[i].SetConnection(conn)
if class == "" || node1 <= 0 || node2 <= 0 {
return fmt.Errorf("Invalid relationship: %v, %v, %v", class, node1, node2)
}
valueArgs = append(
valueArgs,
class,
node1,
node2,
metadata,
)
}
sql := fmt.Sprintf(`
INSERT INTO relationships
(_class, node_from, node_to, metadata)
VALUES %s
`, strings.Join(valueStrings, ","))
conn.log(DEBUG, "Bulk creating relationships:", sql, valueArgs)
// Execute and scan returned IDs
_, err = tx.Exec(sql, valueArgs...)
if err != nil {
return fmt.Errorf("bulk insert failed: %w", err)
}
tx.Commit()
return nil
}