generated from alecodes/base-template
184 lines
3.9 KiB
Go
184 lines
3.9 KiB
Go
package synchronizator
|
|
|
|
import (
|
|
sql "database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var DefaultOptions = &Options{
|
|
Logger: os.Stdout,
|
|
Log_level: INFO,
|
|
DANGEROUSLY_DROP_TABLES: false,
|
|
}
|
|
|
|
func New(connection *sql.DB, options *Options) (*db, error) {
|
|
if options == nil {
|
|
options = DefaultOptions
|
|
}
|
|
|
|
conn := db{
|
|
Connection: connection,
|
|
logger: options.Logger,
|
|
log_level: options.Log_level,
|
|
drop_tables: options.DANGEROUSLY_DROP_TABLES,
|
|
}
|
|
|
|
err := conn.bootstrap()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &conn, nil
|
|
}
|
|
|
|
func (conn *db) log(level LogLevel, args ...any) {
|
|
// Only log if the level is the same or lower than the configured
|
|
if level > conn.log_level {
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(conn.logger, "[ %s - %-5s ]: ", time.Now().Format(time.DateTime), level.String())
|
|
fmt.Fprintln(conn.logger, args...)
|
|
}
|
|
|
|
func (conn *db) bootstrap() error {
|
|
conn.log(INFO, "Initializing database...")
|
|
|
|
sql := ""
|
|
|
|
if conn.drop_tables {
|
|
sql += `
|
|
DROP TABLE IF EXISTS nodes;
|
|
DROP TABLE IF EXISTS relationships;
|
|
DROP INDEX IF EXISTS node_class;
|
|
DROP INDEX IF EXISTS relationships_class;
|
|
`
|
|
}
|
|
|
|
sql += `
|
|
CREATE TABLE IF NOT EXISTS nodes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
_class text NOT NULL,
|
|
name TEXT,
|
|
metadata jsonb DEFAULT '{}'
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS node_class on nodes (_class);
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS relationships (
|
|
node_from INTEGER NOT NULL,
|
|
node_to INTEGER NOT NULL,
|
|
_class text NOT NULL,
|
|
PRIMARY KEY (node_from, node_to),
|
|
CHECK (node_from != node_to),
|
|
CONSTRAINT fk_node_from_relationships FOREIGN KEY (node_from) REFERENCES nodes(id),
|
|
CONSTRAINT fk_node_to_relationships FOREIGN KEY (node_to) REFERENCES nodes(id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS relationships_class on relationships (_class);
|
|
`
|
|
|
|
conn.log(DEBUG, sql)
|
|
_, err := conn.Connection.Exec(sql)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conn *db) generateNodeFromStruct(shape any) *Node {
|
|
// Get the type and handle pointer types
|
|
t := reflect.TypeOf(shape)
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
|
|
node := Node{
|
|
_id: -1,
|
|
_class: strings.ToUpper(t.Name()),
|
|
metadata: shape,
|
|
}
|
|
return &node
|
|
}
|
|
|
|
func (conn *db) RegisterNodeClass(shape any) {
|
|
if conn.node_types == nil {
|
|
conn.node_types = make(NodeType)
|
|
}
|
|
|
|
node := conn.generateNodeFromStruct(shape)
|
|
|
|
conn.node_types[node._class] = *node
|
|
|
|
conn.log(DEBUG, "Registered node class: ", node)
|
|
}
|
|
|
|
func (conn *db) NewCollection(shape any) (*Node, error) {
|
|
collection, err := conn.NewNode("collection", shape)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return collection, nil
|
|
}
|
|
|
|
func (conn *db) NewNode(name string, metadata any) (*Node, error) {
|
|
if metadata == nil {
|
|
return nil, fmt.Errorf("metadata cannot be nil")
|
|
}
|
|
|
|
json_metadata, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid metadata format: %w", err)
|
|
}
|
|
|
|
node := conn.generateNodeFromStruct(metadata)
|
|
node.name = name
|
|
|
|
tx, err := conn.Connection.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer tx.Rollback()
|
|
|
|
conn.log(DEBUG, "Creating node:", node)
|
|
|
|
sql := "INSERT INTO nodes (_class, name, metadata) VALUES ($1, $2, $3) RETURNING id;"
|
|
|
|
err = tx.QueryRow(sql, node._class, node.name, json_metadata).Scan(&node._id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return node, nil
|
|
}
|
|
|
|
func (conn *db) GetNode(id int64) (*Node, error) {
|
|
node := Node{_id: id}
|
|
sql := "SELECT _class, metadata FROM nodes WHERE id = $1;"
|
|
conn.log(DEBUG, sql)
|
|
|
|
var metadata []byte
|
|
err := conn.Connection.QueryRow(sql, id).Scan(&node._class, &metadata)
|
|
if err != nil {
|
|
conn.log(DEBUG, err)
|
|
return nil, fmt.Errorf("No row matching id = %v", id)
|
|
}
|
|
|
|
if err := json.Unmarshal(metadata, &node.metadata); err != nil {
|
|
conn.log(ERROR, err)
|
|
return nil, fmt.Errorf("invalid metadata format: %w", err)
|
|
}
|
|
return &node, nil
|
|
}
|