github.com/simpleiot/simpleiot@v0.18.3/client/node.go (about)

     1  package client
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"reflect"
     8  	"sort"
     9  	"time"
    10  
    11  	"github.com/goccy/go-yaml"
    12  	"github.com/google/uuid"
    13  	"github.com/nats-io/nats.go"
    14  	"github.com/simpleiot/simpleiot/data"
    15  )
    16  
    17  // GetNodes over NATS. Maps to the `p.<id>.<parent>` NATS API.
    18  // Returns data.ErrDocumentNotFound if node is not found.
    19  // If parent is set to "none", the edge details are not included
    20  // and the hash is blank.
    21  // If parent is set to "all", then all living instances of the node are returned.
    22  // If parent is set and id is "all", then all child nodes are returned.
    23  // Parent can be set to "root" and id to "all" to fetch the root node(s).
    24  func GetNodes(nc *nats.Conn, parent, id, typ string, includeDel bool) ([]data.NodeEdge, error) {
    25  	if parent == "" {
    26  		parent = "none"
    27  	}
    28  
    29  	if id == "" {
    30  		id = "all"
    31  	}
    32  
    33  	var requestPoints data.Points
    34  
    35  	if includeDel {
    36  		requestPoints = append(requestPoints,
    37  			data.Point{Type: data.PointTypeTombstone, Value: data.BoolToFloat(includeDel)})
    38  	}
    39  
    40  	if typ != "" {
    41  		requestPoints = append(requestPoints,
    42  			data.Point{Type: data.PointTypeNodeType, Text: typ})
    43  	}
    44  
    45  	reqData, err := requestPoints.ToPb()
    46  	if err != nil {
    47  		return nil, fmt.Errorf("Error encoding reqData: %v", err)
    48  	}
    49  
    50  	subject := fmt.Sprintf("nodes.%v.%v", parent, id)
    51  	nodeMsg, err := nc.Request(subject, reqData, time.Second*20)
    52  	if err != nil {
    53  		return []data.NodeEdge{}, err
    54  	}
    55  
    56  	nodes, err := data.PbDecodeNodesRequest(nodeMsg.Data)
    57  
    58  	if err != nil {
    59  		return []data.NodeEdge{}, err
    60  	}
    61  
    62  	return nodes, nil
    63  }
    64  
    65  // GetNodesType gets node of a custom type.
    66  // id and parent work the same as [GetNodes]
    67  // Deleted nodes are not included.
    68  func GetNodesType[T any](nc *nats.Conn, parent, id string) ([]T, error) {
    69  	var x T
    70  	nodeType := data.ToCamelCase(reflect.TypeOf(x).Name())
    71  
    72  	nodes, err := GetNodes(nc, parent, id, nodeType, false)
    73  
    74  	if err != nil {
    75  		return []T{}, err
    76  	}
    77  
    78  	// decode from NodeEdge to custom types
    79  	ret := make([]T, len(nodes))
    80  
    81  	for i, n := range nodes {
    82  		err := data.Decode(data.NodeEdgeChildren{NodeEdge: n, Children: nil}, &ret[i])
    83  		if err != nil {
    84  			log.Println("Error decode node in GetNodeType:", err)
    85  		}
    86  	}
    87  
    88  	return ret, nil
    89  }
    90  
    91  // GetRootNode returns the root node of the instance
    92  func GetRootNode(nc *nats.Conn) (data.NodeEdge, error) {
    93  	rootNodes, err := GetNodes(nc, "root", "all", "", false)
    94  
    95  	if err != nil {
    96  		return data.NodeEdge{}, err
    97  	}
    98  
    99  	if len(rootNodes) == 0 {
   100  		return data.NodeEdge{}, data.ErrDocumentNotFound
   101  	}
   102  
   103  	return rootNodes[0], nil
   104  }
   105  
   106  // GetNodesForUser gets all nodes for a user
   107  func GetNodesForUser(nc *nats.Conn, userID string) ([]data.NodeEdge, error) {
   108  	var none []data.NodeEdge
   109  	var ret []data.NodeEdge
   110  	userNodes, err := GetNodes(nc, "all", userID, "", false)
   111  	if err != nil {
   112  		return none, err
   113  	}
   114  
   115  	var getChildren func(id string) ([]data.NodeEdge, error)
   116  
   117  	// getNodesHelper recursively gets children of a node
   118  	getChildren = func(id string) ([]data.NodeEdge, error) {
   119  		var ret []data.NodeEdge
   120  
   121  		children, err := GetNodes(nc, id, "all", "", false)
   122  		if err != nil {
   123  			return nil, err
   124  		}
   125  
   126  		for _, c := range children {
   127  			grands, err := getChildren(c.ID)
   128  			if err != nil {
   129  				return nil, err
   130  			}
   131  
   132  			ret = append(ret, grands...)
   133  		}
   134  
   135  		ret = append(ret, children...)
   136  
   137  		return ret, nil
   138  	}
   139  
   140  	// go through parents of root nodes and recursively get all children
   141  	for _, un := range userNodes {
   142  		parents, err := GetNodes(nc, "all", un.Parent, "", false)
   143  		if err != nil {
   144  			return none, fmt.Errorf("Error getting root node: %v", err)
   145  		}
   146  
   147  		// The frontend expects the top level nodes to have Parent set
   148  		// to root
   149  		for i := range parents {
   150  			parents[i].Parent = "root"
   151  		}
   152  
   153  		ret = append(ret, parents...)
   154  		c, err := getChildren(un.Parent)
   155  		if err != nil {
   156  			return none, fmt.Errorf("Error getting children: %v", err)
   157  		}
   158  		ret = append(ret, c...)
   159  	}
   160  
   161  	ret = data.RemoveDuplicateNodesIDParent(ret)
   162  
   163  	return ret, nil
   164  }
   165  
   166  // SendNode is used to send a node to a nats server. Can be
   167  // used to create nodes.
   168  func SendNode(nc *nats.Conn, node data.NodeEdge, origin string) error {
   169  
   170  	if origin != "" {
   171  		for i := range node.Points {
   172  			if node.Points[i].Origin == "" {
   173  				node.Points[i].Origin = origin
   174  			}
   175  		}
   176  
   177  		for i := range node.EdgePoints {
   178  			if node.EdgePoints[i].Origin == "" {
   179  				node.EdgePoints[i].Origin = origin
   180  			}
   181  		}
   182  	}
   183  
   184  	// we need to send the edge points first if we are creating
   185  	// a new node, otherwise the upstream will detect an ophraned node
   186  	// and create a new edge to the root node
   187  	points := node.Points
   188  
   189  	if node.ID == "" {
   190  		return errors.New("ID must be set")
   191  	}
   192  
   193  	if node.Parent == "" || node.Parent == "none" {
   194  		return errors.New("Parent must be set when sending a node")
   195  	}
   196  
   197  	err := SendNodePoints(nc, node.ID, points, true)
   198  
   199  	if err != nil {
   200  		return fmt.Errorf("Error sending node: %v", err)
   201  	}
   202  
   203  	if len(node.EdgePoints) <= 0 {
   204  		// edge should always have a tombstone point, set to false for root node
   205  		node.EdgePoints = []data.Point{{Time: time.Now(),
   206  			Type: data.PointTypeTombstone, Origin: origin}}
   207  	}
   208  
   209  	node.EdgePoints = append(node.EdgePoints, data.Point{
   210  		Type:   data.PointTypeNodeType,
   211  		Text:   node.Type,
   212  		Origin: origin,
   213  	})
   214  
   215  	err = SendEdgePoints(nc, node.ID, node.Parent, node.EdgePoints, true)
   216  	if err != nil {
   217  		return fmt.Errorf("Error sending edge points: %w", err)
   218  
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  // SendNodeType is used to send a node to a nats server. Can be
   225  // used to create nodes.
   226  func SendNodeType[T any](nc *nats.Conn, node T, origin string) error {
   227  	ne, err := data.Encode(node)
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	return SendNode(nc, ne, origin)
   233  }
   234  
   235  func duplicateNodeHelper(nc *nats.Conn, node data.NodeEdge, newParent, origin string) error {
   236  	children, err := GetNodes(nc, node.ID, "all", "", false)
   237  	if err != nil {
   238  		return fmt.Errorf("GetNodes error: %v", err)
   239  	}
   240  
   241  	// create new ID for duplicate node
   242  	node.ID = uuid.New().String()
   243  	node.Parent = newParent
   244  
   245  	err = SendNode(nc, node, origin)
   246  	if err != nil {
   247  		return fmt.Errorf("SendNode error: %v", err)
   248  	}
   249  
   250  	for _, c := range children {
   251  		err := duplicateNodeHelper(nc, c, node.ID, origin)
   252  		if err != nil {
   253  			return err
   254  		}
   255  	}
   256  
   257  	return nil
   258  }
   259  
   260  // DuplicateNode is used to Duplicate a node and all its children
   261  func DuplicateNode(nc *nats.Conn, id, newParent, origin string) error {
   262  	nodes, err := GetNodes(nc, "all", id, "", false)
   263  	if err != nil {
   264  		return fmt.Errorf("GetNode error: %v", err)
   265  	}
   266  
   267  	if len(nodes) < 1 {
   268  		return fmt.Errorf("No nodes returned")
   269  	}
   270  
   271  	node := nodes[0]
   272  
   273  	switch node.Type {
   274  	case data.NodeTypeUser:
   275  		lastName, _ := node.Points.Text(data.PointTypeLastName, "0")
   276  		lastName = lastName + " (Duplicate)"
   277  		node.AddPoint(data.Point{Type: data.PointTypeLastName, Key: "0", Text: lastName})
   278  	default:
   279  		desc := node.Desc() + " (Duplicate)"
   280  		node.AddPoint(data.Point{Type: data.PointTypeDescription, Key: "0", Text: desc})
   281  	}
   282  
   283  	return duplicateNodeHelper(nc, node, newParent, origin)
   284  }
   285  
   286  // DeleteNode removes a node from the specified parent node
   287  func DeleteNode(nc *nats.Conn, id, parent string, origin string) error {
   288  	err := SendEdgePoint(nc, id, parent, data.Point{
   289  		Type:   data.PointTypeTombstone,
   290  		Value:  1,
   291  		Origin: origin,
   292  	}, true)
   293  
   294  	return err
   295  }
   296  
   297  // MoveNode moves a node from one parent to another
   298  func MoveNode(nc *nats.Conn, id, oldParent, newParent, origin string) error {
   299  	if newParent == oldParent {
   300  		return errors.New("can't move node to itself")
   301  	}
   302  
   303  	// fetch the node because we need to know its type
   304  	nodes, err := GetNodes(nc, "all", id, "", true)
   305  	if err != nil {
   306  		return err
   307  	}
   308  
   309  	if len(nodes) < 1 {
   310  		return errors.New("Error fetching node to get type")
   311  	}
   312  
   313  	err = SendEdgePoints(nc, id, newParent, data.Points{
   314  		{Type: data.PointTypeTombstone, Value: 0, Origin: origin},
   315  		{Type: data.PointTypeNodeType, Text: nodes[0].Type, Origin: origin},
   316  	}, true)
   317  
   318  	if err != nil {
   319  		return err
   320  	}
   321  
   322  	err = SendEdgePoint(nc, id, oldParent, data.Point{
   323  		Type:  data.PointTypeTombstone,
   324  		Value: 1,
   325  	}, true)
   326  
   327  	if err != nil {
   328  		return err
   329  	}
   330  
   331  	return nil
   332  }
   333  
   334  // MirrorNode adds a an existing node to a new parent. A node can have
   335  // multiple parents.
   336  func MirrorNode(nc *nats.Conn, id, newParent, origin string) error {
   337  	// fetch the node because we need to know its type
   338  	nodes, err := GetNodes(nc, "all", id, "", true)
   339  	if err != nil {
   340  		return err
   341  	}
   342  
   343  	if len(nodes) < 1 {
   344  		return errors.New("Error fetching node to get type")
   345  	}
   346  
   347  	err = SendEdgePoints(nc, id, newParent, data.Points{
   348  		{Type: data.PointTypeTombstone, Value: 0, Origin: origin},
   349  		{Type: data.PointTypeNodeType, Text: nodes[0].Type, Origin: origin},
   350  	}, true)
   351  
   352  	return err
   353  }
   354  
   355  // NodeWatcher creates a node watcher. update() is called any time there is an update.
   356  // Stop can be called to stop the watcher. get() can be called to get the current value.
   357  func NodeWatcher[T any](nc *nats.Conn, id, parent string) (get func() T, stop func(), err error) {
   358  	stopCh := make(chan struct{})
   359  	var current T
   360  
   361  	pointUpdates := make(chan []data.Point)
   362  	edgeUpdates := make(chan []data.Point)
   363  
   364  	// create subscriptions first so that we get any updates that might happen between the
   365  	// time we fetch node and start subscriptions
   366  
   367  	stopPointSub, err := SubscribePoints(nc, id, func(points []data.Point) {
   368  		pointUpdates <- points
   369  	})
   370  	if err != nil {
   371  		return nil, nil, fmt.Errorf("Point subscribe failed: %v", err)
   372  	}
   373  
   374  	stopEdgeSub, err := SubscribeEdgePoints(nc, id, parent, func(points []data.Point) {
   375  		edgeUpdates <- points
   376  	})
   377  	if err != nil {
   378  		return nil, nil, fmt.Errorf("Edge point subscribe failed: %v", err)
   379  	}
   380  
   381  	nodes, err := GetNodesType[T](nc, parent, id)
   382  	if err != nil {
   383  		if err != data.ErrDocumentNotFound {
   384  			return nil, nil, fmt.Errorf("Error getting node: %v", err)
   385  		}
   386  		// if document is not found, that is OK, points will populate it once they come in
   387  	}
   388  
   389  	// FIXME: we may still have a race condition where older point updates will overwrite
   390  	// a new update when we fetch the node.
   391  	if len(nodes) > 0 {
   392  		current = nodes[0]
   393  	}
   394  
   395  	getCurrent := make(chan chan T)
   396  
   397  	// main loop for watcher. All data access must go through the main
   398  	// loop to avoid race conditions.
   399  	go func() {
   400  		for {
   401  			select {
   402  			case <-stopCh:
   403  				return
   404  			case r := <-getCurrent:
   405  				r <- current
   406  			case pts := <-pointUpdates:
   407  				err := data.MergePoints(id, pts, &current)
   408  				if err != nil {
   409  					log.Println("NodeWatcher, error merging points:", err)
   410  				}
   411  			case pts := <-edgeUpdates:
   412  				err := data.MergeEdgePoints(id, parent, pts, &current)
   413  				if err != nil {
   414  					log.Println("NodeWatcher, error merging edge points:", err)
   415  				}
   416  			}
   417  		}
   418  	}()
   419  
   420  	return func() T {
   421  			ret := make(chan T)
   422  			getCurrent <- ret
   423  			return <-ret
   424  		}, func() {
   425  			stopPointSub()
   426  			stopEdgeSub()
   427  			close(stopCh)
   428  		}, nil
   429  }
   430  
   431  // SiotExport is the format used for exporting and importing data (currently YAML)
   432  type SiotExport struct {
   433  	Nodes []data.NodeEdgeChildren
   434  }
   435  
   436  // ExportNodes is used to export nodes at a particular location to YAML
   437  // The YAML format looks like:
   438  //
   439  //	nodes:
   440  //	- id: inst1
   441  //	  type: device
   442  //	  parent: root
   443  //	  points:
   444  //	  - type: versionApp
   445  //	  children:
   446  //	  - id: d7f5bbe9-a300-4197-93fa-b8e5e07f683a
   447  //	    type: user
   448  //	    parent: inst1
   449  //	    points:
   450  //	    - type: firstName
   451  //	      text: admin
   452  //	    - type: lastName
   453  //	      text: user
   454  //	    - type: phone
   455  //	    - type: email
   456  //	      text: admin
   457  //	    - type: pass
   458  //	      text: admin
   459  //
   460  // Key="0" and Tombstone points with value set to 0 are removed from the export to make
   461  // it easier to read.
   462  func ExportNodes(nc *nats.Conn, id string) ([]byte, error) {
   463  	if id == "root" || id == "" {
   464  		root, err := GetRootNode(nc)
   465  		if err != nil {
   466  			return nil, fmt.Errorf("Error getting root node: %w", err)
   467  		}
   468  		id = root.ID
   469  	}
   470  
   471  	rootNodes, err := GetNodes(nc, "all", id, "", false)
   472  	if err != nil {
   473  		return nil, fmt.Errorf("Error getting root nodes: %w", err)
   474  	}
   475  
   476  	if len(rootNodes) < 1 {
   477  		return nil, fmt.Errorf("no root nodes returned")
   478  	}
   479  
   480  	var necNodes []data.NodeEdgeChildren
   481  
   482  	// we only export one node as there may be multiple mirrors of the node in the tree
   483  	nec := data.NodeEdgeChildren{NodeEdge: rootNodes[0], Children: nil}
   484  	err = exportNodesHelper(nc, &nec)
   485  	if err != nil {
   486  		return nil, err
   487  	}
   488  
   489  	necNodes = append(necNodes, nec)
   490  
   491  	ne := SiotExport{Nodes: necNodes}
   492  
   493  	return yaml.Marshal(ne)
   494  }
   495  
   496  func exportNodesHelper(nc *nats.Conn, node *data.NodeEdgeChildren) error {
   497  	// sort edge and node points
   498  	sort.Sort(data.ByTypeKey(node.Points))
   499  	sort.Sort(data.ByTypeKey(node.EdgePoints))
   500  	// reduce a little noise ...
   501  	// remove tombstone "0" edge points as that does not convey much information
   502  	// also remove and key="0" fields in points
   503  	for i, p := range node.Points {
   504  		if p.Key == "0" {
   505  			node.Points[i].Key = ""
   506  		}
   507  	}
   508  
   509  	for i, p := range node.EdgePoints {
   510  		if p.Key == "0" {
   511  			node.EdgePoints[i].Key = ""
   512  		}
   513  	}
   514  
   515  	// remove tombstone 0 edge points
   516  	i := 0
   517  	for _, p := range node.EdgePoints {
   518  		if p.Type == data.PointTypeTombstone && p.Value == 0 {
   519  			continue
   520  		}
   521  		node.EdgePoints[i] = p
   522  		i++
   523  	}
   524  
   525  	node.EdgePoints = node.EdgePoints[:i]
   526  
   527  	children, err := GetNodes(nc, node.ID, "all", "", false)
   528  	if err != nil {
   529  		return fmt.Errorf("Error getting children: %w", err)
   530  	}
   531  
   532  	for _, c := range children {
   533  		nec := data.NodeEdgeChildren{NodeEdge: c, Children: nil}
   534  		err := exportNodesHelper(nc, &nec)
   535  		if err != nil {
   536  			return err
   537  		}
   538  
   539  		node.Children = append(node.Children, nec)
   540  	}
   541  
   542  	return nil
   543  }
   544  
   545  // ImportNodes is used to import nodes at a location in YAML format. New IDs
   546  // are generated for all nodes unless preserve IDs is set to true.
   547  // If there multiple references to the same ID,
   548  // then an attempt is made to replace all of these with the new ID.  This also
   549  // allows you to use "friendly" ID names in hand generated YAML files.
   550  func ImportNodes(nc *nats.Conn, parent string, yamlData []byte, origin string, preserveIDs bool) error {
   551  	// first make sure the parent node exists
   552  	var rootNode data.NodeEdge
   553  	if parent == "root" || parent == "" {
   554  		var err error
   555  		rootNode, err = GetRootNode(nc)
   556  		if err != nil {
   557  			return err
   558  		}
   559  	} else {
   560  		n, err := GetNodes(nc, "all", parent, "", false)
   561  		if err != nil {
   562  			return err
   563  		}
   564  		if len(n) < 1 {
   565  			return fmt.Errorf("Parent node \"%v\" not found", parent)
   566  		}
   567  	}
   568  
   569  	var imp SiotExport
   570  
   571  	err := yaml.Unmarshal(yamlData, &imp)
   572  	if err != nil {
   573  		return fmt.Errorf("Error parsing YAML data: %w", err)
   574  	}
   575  
   576  	var importHelper func(data.NodeEdgeChildren) error
   577  	importHelper = func(node data.NodeEdgeChildren) error {
   578  		err := SendNode(nc, node.NodeEdge, origin)
   579  		if err != nil {
   580  			return fmt.Errorf("Error sending node: %w", err)
   581  		}
   582  
   583  		for _, c := range node.Children {
   584  			err := importHelper(c)
   585  			if err != nil {
   586  				return err
   587  			}
   588  		}
   589  		return nil
   590  	}
   591  
   592  	if len(imp.Nodes) < 1 {
   593  		return fmt.Errorf("Error: imported data did not have any nodes")
   594  	}
   595  
   596  	// set parent of first node
   597  	imp.Nodes[0].Parent = parent
   598  
   599  	// append (import) to top level node description
   600  	for i, p := range imp.Nodes[0].Points {
   601  		if p.Type == data.PointTypeDescription {
   602  			imp.Nodes[0].Points[i].Text += " (import)"
   603  		}
   604  	}
   605  
   606  	if preserveIDs {
   607  		err := checkIDs(imp.Nodes[0], parent)
   608  		if err != nil {
   609  			return err
   610  		}
   611  	} else {
   612  		ReplaceIDs(&imp.Nodes[0], parent)
   613  	}
   614  
   615  	err = importHelper(imp.Nodes[0])
   616  
   617  	// if we imported the root node, then we have to tombstone the old root node
   618  	if parent == "root" && rootNode.ID != imp.Nodes[0].ID {
   619  		err := DeleteNode(nc, rootNode.ID, parent, "import")
   620  		if err != nil {
   621  			return fmt.Errorf("Error deleting old root node: %w", err)
   622  		}
   623  	}
   624  
   625  	return err
   626  }
   627  
   628  func checkIDs(node data.NodeEdgeChildren, parent string) error {
   629  	if parent == "" {
   630  		return fmt.Errorf("parent must be specified")
   631  	}
   632  
   633  	if node.Parent != parent {
   634  		return fmt.Errorf("node parent %v does not match parent %v", node.Parent, parent)
   635  	}
   636  
   637  	if node.ID == "" {
   638  		return fmt.Errorf("ID cannot be blank")
   639  	}
   640  
   641  	for _, c := range node.Children {
   642  		err := checkIDs(c, node.ID)
   643  		if err != nil {
   644  			return err
   645  		}
   646  	}
   647  
   648  	return nil
   649  }
   650  
   651  // ReplaceIDs is used to replace IDs tree of nodes.
   652  // If there multiple references to the same ID,
   653  // then an attempt is made to replace all of these with the new ID.
   654  // This function modifies the tree that is passed in.
   655  // Replace IDs also updates the partent fields.
   656  func ReplaceIDs(nodes *data.NodeEdgeChildren, parent string) {
   657  	// idMap is used to translate old IDs to new
   658  	idMap := make(map[string]string)
   659  
   660  	var replaceHelper func(*data.NodeEdgeChildren, string)
   661  	replaceHelper = func(n *data.NodeEdgeChildren, parent string) {
   662  		n.Parent = parent
   663  		// update node ID
   664  		var newID string
   665  		if n.ID == "" {
   666  			// always assign a new ID if blank
   667  			newID = uuid.New().String()
   668  		} else {
   669  			var ok bool
   670  			newID, ok = idMap[n.ID]
   671  			if !ok {
   672  				newID = uuid.New().String()
   673  				idMap[n.ID] = newID
   674  			}
   675  		}
   676  		n.ID = newID
   677  
   678  		// check for any points that might have node hashes
   679  		for i, p := range n.Points {
   680  			if p.Type == data.PointTypeNodeID {
   681  				if p.Text == "" {
   682  					continue
   683  				}
   684  				newID, ok := idMap[p.Text]
   685  				if !ok {
   686  					newID = uuid.New().String()
   687  					idMap[p.Text] = newID
   688  				}
   689  				n.Points[i].Text = newID
   690  			}
   691  		}
   692  
   693  		for i := range n.Children {
   694  			replaceHelper(&n.Children[i], n.ID)
   695  		}
   696  	}
   697  
   698  	replaceHelper(nodes, parent)
   699  }