https://github.com/google/cayley
Raw File
Tip revision: 84f783def255c1de2b3352b77cdea1d6be772df1 authored by Iddan Aaronsohn on 29 June 2019, 12:23:28 UTC
Merge branch 'master' into master
Tip revision: 84f783d
traversals.go
// Copyright 2017 The Cayley Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gizmo

// Adds special traversal functions to JS Gizmo objects. Most of these just build the chain of objects, and won't often need the session.

import (
	"fmt"

	"github.com/dop251/goja"

	"github.com/cayleygraph/cayley/graph"
	"github.com/cayleygraph/cayley/graph/iterator"
	"github.com/cayleygraph/cayley/graph/path"
	"github.com/cayleygraph/cayley/graph/shape"
)

// pathObject is a Path object in Gizmo.
//
// Both `.Morphism()` and `.Vertex()` create path objects, which provide the following traversal methods.
// Note that `.Vertex()` returns a query object, which is a subclass of path object.
//
// For these examples, suppose we have the following graph:
//
//	+-------+                        +------+
//	| alice |-----                 ->| fred |<--
//	+-------+     \---->+-------+-/  +------+   \-+-------+
//	              ----->| #bob# |       |         |*emily*|
//	+---------+--/  --->+-------+       |         +-------+
//	| charlie |    /                    v
//	+---------+   /                  +--------+
//	  \---    +--------+             |*#greg#*|
//	      \-->| #dani# |------------>+--------+
//	          +--------+
//
// Where every link is a `<follows>` relationship, and the nodes with an extra `#` in the name have an extra `<status>` link. As in,
//
//	<dani> -- <status> --> "cool_person"
//
// Perhaps these are the influencers in our community. So too are extra `*`s in the name -- these are our smart people,
// according to the `<smart_graph>` label, eg, the quad:
//
//	<greg> <status> "smart_person" <smart_graph> .
type pathObject struct {
	s      *Session
	finals bool
	path   *path.Path
}

func (p *pathObject) new(np *path.Path) *pathObject {
	return &pathObject{
		s:      p.s,
		finals: p.finals,
		path:   np,
	}
}

func (p *pathObject) newVal(np *path.Path) goja.Value {
	return p.s.vm.ToValue(p.new(np))
}
func (p *pathObject) clonePath() *path.Path {
	np := p.path.Clone()
	// most likely path will be continued, so we'll put non-capped stack slice
	// into new path object instead of preserving it in an old one
	p.path, np = np, p.path
	return np
}
func (p *pathObject) buildIteratorTree() graph.Iterator {
	if p.path == nil {
		return iterator.NewNull()
	}
	return p.path.BuildIteratorOn(p.s.qs)
}

// Filter all paths to ones which, at this point, are on the given node.
// Signature: (node, [node..])
//
// Arguments:
//
// * `node`: A string for a node. Can be repeated or a list of strings.
//
// Example:
//	// javascript
//	// Starting from all nodes in the graph, find the paths that follow bob.
//	// Results in three paths for bob (from alice, charlie and dani).All()
//	g.V().Out("<follows>").Is("<bob>").All()
func (p *pathObject) Is(call goja.FunctionCall) goja.Value {
	args, err := toQuadValues(exportArgs(call.Arguments))
	if err != nil {
		return throwErr(p.s.vm, err)
	}
	np := p.clonePath().Is(args...)
	return p.newVal(np)
}
func (p *pathObject) inout(call goja.FunctionCall, in bool) goja.Value {
	preds, tags, ok := toViaData(exportArgs(call.Arguments))
	if !ok {
		return throwErr(p.s.vm, errNoVia)
	}
	np := p.clonePath()
	if in {
		np = np.InWithTags(tags, preds...)
	} else {
		np = np.OutWithTags(tags, preds...)
	}
	return p.newVal(np)
}

// In is inverse of Out.
// Starting with the nodes in `path` on the object, follow the quads with predicates defined by `predicatePath` to their subjects.
// Signature: ([predicatePath], [tags])
//
// Arguments:
//
// * `predicatePath` (Optional): One of:
//   * null or undefined: All predicates pointing into this node
//   * a string: The predicate name to follow into this node
//   * a list of strings: The predicates to follow into this node
//   * a query path object: The target of which is a set of predicates to follow.
// * `tags` (Optional): One of:
//   * null or undefined: No tags
//   * a string: A single tag to add the predicate used to the output set.
//   * a list of strings: Multiple tags to use as keys to save the predicate used to the output set.
//
// Example:
//
//	// javascript
//	// Find the cool people, bob, dani and greg
//	g.V("cool_person").In("<status>").All()
//	// Find who follows bob, in this case, alice, charlie, and dani
//	g.V("<bob>").In("<follows>").All()
//	// Find who follows the people emily follows, namely, bob and emily
//	g.V("<emily>").Out("<follows>").In("<follows>").All()
func (p *pathObject) In(call goja.FunctionCall) goja.Value {
	return p.inout(call, true)
}

// Out is the work-a-day way to get between nodes, in the forward direction.
// Starting with the nodes in `path` on the subject, follow the quads with predicates defined by `predicatePath` to their objects.
// Signature: ([predicatePath], [tags])
//
// Arguments:
//
// * `predicatePath` (Optional): One of:
//   * null or undefined: All predicates pointing out from this node
//   * a string: The predicate name to follow out from this node
//   * a list of strings: The predicates to follow out from this node
//   * a query path object: The target of which is a set of predicates to follow.
// * `tags` (Optional): One of:
//   * null or undefined: No tags
//   * a string: A single tag to add the predicate used to the output set.
//   * a list of strings: Multiple tags to use as keys to save the predicate used to the output set.
//
// Example:
//
//	// javascript
//	// The working set of this is bob and dani
//	g.V("<charlie>").Out("<follows>").All()
//	// The working set of this is fred, as alice follows bob and bob follows fred.
//	g.V("<alice>").Out("<follows>").Out("<follows>").All()
//	// Finds all things dani points at. Result is bob, greg and cool_person
//	g.V("<dani>").Out().All()
//	// Finds all things dani points at on the status linkage.
//	// Result is bob, greg and cool_person
//	g.V("<dani>").Out(["<follows>", "<status>"]).All()
//	// Finds all things dani points at on the status linkage, given from a separate query path.
//	// Result is {"id": "cool_person", "pred": "<status>"}
//	g.V("<dani>").Out(g.V("<status>"), "pred").All()
func (p *pathObject) Out(call goja.FunctionCall) goja.Value {
	return p.inout(call, false)
}

// Both follow the predicate in either direction. Same as Out or In.
// Signature: ([predicatePath], [tags])
//
// Example:
//	// javascript
//	// Find all followers/followees of fred. Returns bob, emily and greg
//	g.V("<fred>").Both("<follows>").All()
func (p *pathObject) Both(call goja.FunctionCall) goja.Value {
	preds, tags, ok := toViaData(exportArgs(call.Arguments))
	if !ok {
		return throwErr(p.s.vm, errNoVia)
	}
	np := p.clonePath().BothWithTags(tags, preds...)
	return p.newVal(np)
}
func (p *pathObject) follow(ep *pathObject, rev bool) *pathObject {
	if ep == nil {
		return p
	}
	np := p.clonePath()
	if rev {
		np = np.FollowReverse(ep.path)
	} else {
		np = np.Follow(ep.path)
	}
	return p.new(np)
}

// Follow is the way to use a path prepared with Morphism. Applies the path chain on the morphism object to the current path.
//
// Starts as if at the g.M() and follows through the morphism path.
//
// Example:
// 	// javascript:
//	var friendOfFriend = g.Morphism().Out("<follows>").Out("<follows>")
//	// Returns the followed people of who charlie follows -- a simplistic "friend of my friend"
//	// and whether or not they have a "cool" status. Potential for recommending followers abounds.
//	// Returns bob and greg
//	g.V("<charlie>").Follow(friendOfFriend).Has("<status>", "cool_person").All()
func (p *pathObject) Follow(path *pathObject) *pathObject {
	return p.follow(path, false)
}

// FollowR is the same as Follow but follows the chain in the reverse direction. Flips "In" and "Out" where appropriate,
// the net result being a virtual predicate followed in the reverse direction.
//
// Starts at the end of the morphism and follows it backwards (with appropriate flipped directions) to the g.M() location.
//
// Example:
// 	// javascript:
//	var friendOfFriend = g.Morphism().Out("<follows>").Out("<follows>")
//	// Returns the third-tier of influencers -- people who follow people who follow the cool people.
//	// Returns charlie (from bob), charlie (from greg), bob and emily
//	g.V().Has("<status>", "cool_person").FollowR(friendOfFriend).All()
func (p *pathObject) FollowR(path *pathObject) *pathObject {
	return p.follow(path, true)
}

// FollowRecursive is the same as Follow but follows the chain recursively.
//
// Starts as if at the g.M() and follows through the morphism path multiple times, returning all nodes encountered.
//
// Example:
// 	// javascript:
//	var friend = g.Morphism().Out("<follows>")
//	// Returns all people in Charlie's network.
//	// Returns bob and dani (from charlie), fred (from bob) and greg (from dani).
//	g.V("<charlie>").FollowRecursive(friend).All()
func (p *pathObject) FollowRecursive(call goja.FunctionCall) goja.Value {
	preds, maxDepth, tags, ok := toViaDepthData(exportArgs(call.Arguments))
	if !ok || len(preds) == 0 {
		return throwErr(p.s.vm, errNoVia)
	} else if len(preds) != 1 {
		return throwErr(p.s.vm, fmt.Errorf("expected one predicate or path for recursive follow"))
	}
	np := p.clonePath()
	np = np.FollowRecursive(preds[0], maxDepth, tags)
	return p.newVal(np)
}

// And is an alias for Intersect.
func (p *pathObject) And(path *pathObject) *pathObject {
	return p.Intersect(path)
}

// Intersect filters all paths by the result of another query path.
//
// This is essentially a join where, at the stage of each path, a node is shared.
// Example:
// 	// javascript
//	var cFollows = g.V("<charlie>").Out("<follows>")
//	var dFollows = g.V("<dani>").Out("<follows>")
//	// People followed by both charlie (bob and dani) and dani (bob and greg) -- returns bob.
//	cFollows.Intersect(dFollows).All()
//	// Equivalently, g.V("<charlie>").Out("<follows>").And(g.V("<dani>").Out("<follows>")).All()
func (p *pathObject) Intersect(path *pathObject) *pathObject {
	if path == nil {
		return p
	}
	np := p.clonePath().And(path.path)
	return p.new(np)
}

// Union returns the combined paths of the two queries.
//
// Notice that it's per-path, not per-node. Once again, if multiple paths reach the same destination,
// they might have had different ways of getting there (and different tags).
// See also: `path.Tag()`
//
// Example:
// 	// javascript
//	var cFollows = g.V("<charlie>").Out("<follows>")
//	var dFollows = g.V("<dani>").Out("<follows>")
//	// People followed by both charlie (bob and dani) and dani (bob and greg) -- returns bob (from charlie), dani, bob (from dani), and greg.
//	cFollows.Union(dFollows).All()
func (p *pathObject) Union(path *pathObject) *pathObject {
	if path == nil {
		return p
	}
	np := p.clonePath().Or(path.path)
	return p.new(np)
}

// Or is an alias for Union.
func (p *pathObject) Or(path *pathObject) *pathObject {
	return p.Union(path)
}

// Back returns current path to a set of nodes on a given tag, preserving all constraints.
//
// If still valid, a path will now consider their vertex to be the same one as the previously tagged one,
// with the added constraint that it was valid all the way here.
// Useful for traversing back in queries and taking another route for things that have matched so far.
//
// Arguments:
//
// * `tag`: A previous tag in the query to jump back to.
//
// Example:
// 	// javascript
//	// Start from all nodes, save them into start, follow any status links,
//	// jump back to the starting node, and find who follows them. Return the result.
//	// Results are:
//	//   {"id": "<alice>", "start": "<bob>"},
//	//   {"id": "<charlie>", "start": "<bob>"},
//	//   {"id": "<charlie>", "start": "<dani>"},
//	//   {"id": "<dani>", "start": "<bob>"},
//	//   {"id": "<dani>", "start": "<greg>"},
//	//   {"id": "<dani>", "start": "<greg>"},
//	//   {"id": "<fred>", "start": "<greg>"},
//	//   {"id": "<fred>", "start": "<greg>"}
//	g.V().Tag("start").Out("<status>").Back("start").In("<follows>").All()
func (p *pathObject) Back(tag string) *pathObject {
	np := p.clonePath().Back(tag)
	return p.new(np)
}

// Tag saves a list of nodes to a given tag.
//
// In order to save your work or learn more about how a path got to the end, we have tags.
// The simplest thing to do is to add a tag anywhere you'd like to put each node in the result set.
//
// Arguments:
//
// * `tag`: A string or list of strings to act as a result key. The value for tag was the vertex the path was on at the time it reached "Tag"
// Example:
// 	// javascript
//	// Start from all nodes, save them into start, follow any status links, and return the result.
//	// Results are:
//	//   {"id": "cool_person", "start": "<bob>"},
//	//   {"id": "cool_person", "start": "<dani>"},
//	//   {"id": "cool_person", "start": "<greg>"},
//	//   {"id": "smart_person", "start": "<emily>"},
//	//   {"id": "smart_person", "start": "<greg>"}
//	g.V().Tag("start").Out("<status>").All()
func (p *pathObject) Tag(tags ...string) *pathObject {
	np := p.clonePath().Tag(tags...)
	return p.new(np)
}

// As is an alias for Tag.
func (p *pathObject) As(tags ...string) *pathObject {
	return p.Tag(tags...)
}

// Has filters all paths which are, at this point, on the subject for the given predicate and object,
// but do not follow the path, merely filter the possible paths.
//
// Usually useful for starting with all nodes, or limiting to a subset depending on some predicate/value pair.
//
// Signature: (predicate, object)
//
// Arguments:
//
// * `predicate`: A string for a predicate node.
// * `object`: A string for a object node or a set of filters to find it.
//
// Example:
// 	// javascript
//	// Start from all nodes that follow bob -- results in alice, charlie and dani
//	g.V().Has("<follows>", "<bob>").All()
//	// People charlie follows who then follow fred. Results in bob.
//	g.V("<charlie>").Out("<follows>").Has("<follows>", "<fred>").All()
//	// People with friends who have names sorting lower then "f".
//	g.V().Has("<follows>", gt("<f>")).All()
func (p *pathObject) Has(call goja.FunctionCall) goja.Value {
	return p.has(call, false)
}

// HasR is the same as Has, but sets constraint in reverse direction.
func (p *pathObject) HasR(call goja.FunctionCall) goja.Value {
	return p.has(call, true)
}
func (p *pathObject) has(call goja.FunctionCall, rev bool) goja.Value {
	args := exportArgs(call.Arguments)
	if len(args) == 0 {
		return throwErr(p.s.vm, errArgCount{Got: len(args)})
	}
	via := args[0]
	args = args[1:]
	if vp, ok := via.(*pathObject); ok {
		via = vp.path
	} else {
		var err error
		via, err = toQuadValue(via)
		if err != nil {
			return throwErr(p.s.vm, err)
		}
	}
	if len(args) > 0 {
		var filt []shape.ValueFilter
	loop:
		for _, a := range args {
			switch a := a.(type) {
			case valFilter:
				filt = append(filt, a.f)
			case []valFilter:
				for _, s := range a {
					filt = append(filt, s.f)
				}
			default:
				filt = nil
				// failed to collect all argument as filters - switch back to nodes-only mode
				break loop
			}
		}
		if len(filt) > 0 {
			np := p.clonePath()
			np = np.HasFilter(via, rev, filt...)
			return p.newVal(np)
		}
	}
	qv, err := toQuadValues(args)
	if err != nil {
		return throwErr(p.s.vm, err)
	}
	np := p.clonePath()
	if rev {
		np = np.HasReverse(via, qv...)
	} else {
		np = np.Has(via, qv...)
	}
	return p.newVal(np)
}
func (p *pathObject) save(call goja.FunctionCall, rev, opt bool) goja.Value {
	args := exportArgs(call.Arguments)
	if len(args) > 2 || len(args) == 0 {
		return throwErr(p.s.vm, errArgCount{Got: len(args)})
	}
	vtag := args[0]
	if len(args) == 2 {
		vtag = args[1]
	}
	tag, ok := vtag.(string)
	if !ok {
		return throwErr(p.s.vm, fmt.Errorf("expected string, got: %T", vtag))
	}
	via := args[0]
	if vp, ok := via.(*pathObject); ok {
		via = vp.path
	} else {
		var err error
		via, err = toQuadValue(via)
		if err != nil {
			return throwErr(p.s.vm, err)
		}
	}
	np := p.clonePath()
	if opt {
		if rev {
			np = np.SaveOptionalReverse(via, tag)
		} else {
			np = np.SaveOptional(via, tag)
		}
	} else {
		if rev {
			np = np.SaveReverse(via, tag)
		} else {
			np = np.Save(via, tag)
		}
	}
	return p.newVal(np)
}

// Save saves the object of all quads with predicate into tag, without traversal.
// Signature: (predicate, tag)
//
// Arguments:
//
// * `predicate`: A string for a predicate node.
// * `tag`: A string for a tag key to store the object node.
//
// Example:
// 	// javascript
//	// Start from dani and bob and save who they follow into "target"
//	// Returns:
//	//   {"id" : "<bob>", "target": "<fred>" },
//	//   {"id" : "<dani>", "target": "<bob>" },
//	//   {"id" : "<dani>", "target": "<greg>" }
//	g.V("<dani>", "<bob>").Save("<follows>", "target").All()
func (p *pathObject) Save(call goja.FunctionCall) goja.Value {
	return p.save(call, false, false)
}

// SaveR is the same as Save, but tags values via reverse predicate.
func (p *pathObject) SaveR(call goja.FunctionCall) goja.Value {
	return p.save(call, true, false)
}

// SaveOpt is the same as Save, but returns empty tags if predicate does not exists.
func (p *pathObject) SaveOpt(call goja.FunctionCall) goja.Value {
	return p.save(call, false, true)
}

// SaveOptR is the same as SaveOpt, but tags values via reverse predicate.
func (p *pathObject) SaveOptR(call goja.FunctionCall) goja.Value {
	return p.save(call, true, true)
}

// Except removes all paths which match query from current path.
//
// In a set-theoretic sense, this is (A - B). While `g.V().Except(path)` to achieve `U - B = !B` is supported, it's often very slow.
// Example:
// 	// javascript
//	var cFollows = g.V("<charlie>").Out("<follows>")
//	var dFollows = g.V("<dani>").Out("<follows>")
//	// People followed by both charlie (bob and dani) and dani (bob and greg) -- returns bob.
//	cFollows.Except(dFollows).All()   // The set (dani) -- what charlie follows that dani does not also follow.
//	// Equivalently, g.V("<charlie>").Out("<follows>").Except(g.V("<dani>").Out("<follows>")).All()
func (p *pathObject) Except(path *pathObject) *pathObject {
	if path == nil {
		return p
	}
	np := p.clonePath().Except(path.path)
	return p.new(np)
}

// Unique removes duplicate values from the path.
func (p *pathObject) Unique() *pathObject {
	np := p.clonePath().Unique()
	return p.new(np)
}

// Difference is an alias for Except.
func (p *pathObject) Difference(path *pathObject) *pathObject {
	return p.Except(path)
}

// Labels gets the list of inbound and outbound quad labels
func (p *pathObject) Labels() *pathObject {
	np := p.clonePath().Labels()
	return p.new(np)
}

// InPredicates gets the list of predicates that are pointing in to a node.
//
// Example:
// 	// javascript
//	// bob only has "<follows>" predicates pointing inward
//	// returns "<follows>"
//	g.V("<bob>").InPredicates().All()
func (p *pathObject) InPredicates() *pathObject {
	np := p.clonePath().InPredicates()
	return p.new(np)
}

// OutPredicates gets the list of predicates that are pointing out from a node.
//
// Example:
// 	// javascript
//	// bob has "<follows>" and "<status>" edges pointing outwards
//	// returns "<follows>", "<status>"
//	g.V("<bob>").OutPredicates().All()
func (p *pathObject) OutPredicates() *pathObject {
	np := p.clonePath().OutPredicates()
	return p.new(np)
}

// SaveInPredicates tags the list of predicates that are pointing in to a node.
//
// Example:
// 	// javascript
//	// bob only has "<follows>" predicates pointing inward
//	// returns {"id":"<bob>", "pred":"<follows>"}
//	g.V("<bob>").SaveInPredicates("pred").All()
func (p *pathObject) SaveInPredicates(tag string) *pathObject {
	np := p.clonePath().SavePredicates(true, tag)
	return p.new(np)
}

// SaveOutPredicates tags the list of predicates that are pointing out from a node.
//
// Example:
// 	// javascript
//	// bob has "<follows>" and "<status>" edges pointing outwards
//	// returns {"id":"<bob>", "pred":"<follows>"}
//	g.V("<bob>").SaveInPredicates("pred").All()
func (p *pathObject) SaveOutPredicates(tag string) *pathObject {
	np := p.clonePath().SavePredicates(false, tag)
	return p.new(np)
}

// LabelContext sets (or removes) the subgraph context to consider in the following traversals.
// Affects all In(), Out(), and Both() calls that follow it. The default LabelContext is null (all subgraphs).
// Signature: ([labelPath], [tags])
//
// Arguments:
//
// * `predicatePath` (Optional): One of:
//   * null or undefined: In future traversals, consider all edges, regardless of subgraph.
//   * a string: The name of the subgraph to restrict traversals to.
//   * a list of strings: A set of subgraphs to restrict traversals to.
//   * a query path object: The target of which is a set of subgraphs.
// * `tags` (Optional): One of:
//   * null or undefined: No tags
//   * a string: A single tag to add the last traversed label to the output set.
//   * a list of strings: Multiple tags to use as keys to save the label used to the output set.
//
// Example:
// 	// javascript
//	// Find the status of people Dani follows
//	g.V("<dani>").Out("<follows>").Out("<status>").All()
//	// Find only the statuses provided by the smart_graph
//	g.V("<dani>").Out("<follows>").LabelContext("<smart_graph>").Out("<status>").All()
//	// Find all people followed by people with statuses in the smart_graph.
//	g.V().LabelContext("<smart_graph>").In("<status>").LabelContext(null).In("<follows>").All()
func (p *pathObject) LabelContext(call goja.FunctionCall) goja.Value {
	labels, tags, ok := toViaData(exportArgs(call.Arguments))
	if !ok {
		return throwErr(p.s.vm, errNoVia)
	}
	np := p.clonePath().LabelContextWithTags(tags, labels...)
	return p.newVal(np)
}

// Filter applies constraints to a set of nodes. Can be used to filter values by range or match strings.
func (p *pathObject) Filter(args ...valFilter) (*pathObject, error) {
	if len(args) == 0 {
		return nil, errArgCount{Got: len(args)}
	}
	filt := make([]shape.ValueFilter, 0, len(args))
	for _, f := range args {
		filt = append(filt, f.f)
	}
	np := p.clonePath().Filters(filt...)
	return p.new(np), nil
}

// Limit limits a number of nodes for current path.
//
// Arguments:
//
// * `limit`: A number of nodes to limit results to.
//
// Example:
// 	// javascript
//	// Start from all nodes that follow bob, and limit them to 2 nodes -- results in alice and charlie
//	g.V().Has("<follows>", "<bob>").Limit(2).All()
func (p *pathObject) Limit(limit int) *pathObject {
	np := p.clonePath().Limit(int64(limit))
	return p.new(np)
}

// Skip skips a number of nodes for current path.
//
// Arguments:
//
// * `offset`: A number of nodes to skip.
//
// Example:
//	// javascript
//	// Start from all nodes that follow bob, and skip 2 nodes -- results in dani
//	g.V().Has("<follows>", "<bob>").Skip(2).All()
func (p *pathObject) Skip(offset int) *pathObject {
	np := p.clonePath().Skip(int64(offset))
	return p.new(np)
}
back to top