Home Reference Source

lib/Relationship.js

'use strict'

const neo4j = require('neo4j-driver/lib/v1')
const DB = require('./DB')
const { Model } = require('maggoo')

const _model = Symbol()
const _type = Symbol()
const _singular = Symbol()
const _start = Symbol()
const _end = Symbol()
const _direction = Symbol()

/**
 * Class representing a relationship between two Nodes
 */
class Relationship extends Model
{
	// *** CONSTANTS

	/**
	 * @constant
	 * @member {string}
	 */
	static get OUT()
	{
		return 'relationship.direction.out'
	}

	/**
	 * @constant
	 * @member {string}
	 */
	static get IN()
	{
		return 'relationship.direction.in'
	}

	/**
	 * @constant
	 * @type {string}
	 */
	static get BOTH()
	{
		return 'relationship.direction.both'
	}

	/**
	 * @constant
	 * @type {string}
	 */
	static get ANY()
	{
		return 'relationship.direction.any'
	}

	/**
	 * @constant
	 * @type {string}
	 */
	static get DEFAULT_TYPE()
	{
		return 'is_related_to'
	}


	// *** PUBLIC METHODS

	/**
	 * Creates an instance of Relationship
	 *
	 * @param {object|neo4j.types.Relationship} relation Relationship data object or neo4j Relationship
	 * @param {Graph} graph Current graph
	 */
	constructor(relation, graph)
	{
		if (relation instanceof neo4j.types.Relationship)
		{
			super(relation.properties)

			this.$entity = relation
		}
		else
		{
			super(relation)
		}

		this.$graph = graph
	}

	get $direction()
	{
		return this.constructor.direction
	}

	get $type()
	{
		return this.constructor.type
	}

	/**
	 * The neo4j entity ID.
	 * Note that this could change over time, use id instead.
	 *
	 * @readonly
	 * @type {number}
	 */
	get $id()
	{
		return this.$entity ? this.$entity.identity.toNumber() : null
	}

	/**
	 * Whether or not the node is already stored in the graph database.
	 *
	 * @readonly
	 * @type {boolean}
	 */
	get $new()
	{
		return !this.$entity
	}

	/**
	 * Whether or not the Node has changed properties.
	 *
	 * @readonly
	 * @type {boolean}
	 */
	get $dirty()
	{
		return this.$new || super.$dirty
	}

	get start()
	{
		return this[_start]
	}

	set start(start)
	{
		/**
		 * @private
		*/
		this[_start] = start
	}

	get end()
	{
		return this[_end]
	}

	set end(end)
	{
		/**
		 * @private
		*/
		this[_end] = end
	}

	/**
	 * Set property value
	 *
	 * @param {string} key Property name
	 * @param {any} value Property value
	 */
	setProperty(key, value)
	{
		if (value && value.toNumber)
		{
			value = value.toNumber()
		}

		super.setProperty(key, value)
	}

	/**
	 * Save the relationship
	 *
	 * @param {any} [tx] Transaction
	 */
	async save(tx = null)
	{
		let session = null

		if (!tx)
		{
			tx = DB.beginTransaction()
			session = tx.session
		}

		const left = [Relationship.IN].includes(this.$direction) ? '<' : ''
		const right = [Relationship.OUT, Relationship.ANY, Relationship.BOTH].includes(this.$direction) ? '>' : ''

		let create = `MERGE (start)${ left }-[r:${ this.$type }]-${ right }(end)`
		let set = 'SET r += {properties}'

		if (this.$direction === Relationship.BOTH)
		{
			create += `
			MERGE (end)-[r_2:${ this.$type }]->(start)`

			set += `
			SET r_2 += {properties}`
		}

		const query = `
			MATCH (start:${ this.start.$base } { id: {start} }), (end:${ this.end.$base } { id: {end} })
			${ create }
			${ set }
			RETURN r
		`

		const parameters = {
			start: this.start.id,
			end: this.end.id,
			properties: this.$data
		}

		// console.log('save relationship', query, parameters)
		// console.time('save relationship')

		const result = await tx.run(query, parameters).then(r => r)

		this.$entity = result.records[0].get('r')
		this.reset()

		// console.timeEnd('save relationship')

		if (session)
		{
			await tx.commit().then(r => r)

			session.close()
		}
	}

	// *** STATIC METHODS

	/**
	 * The type of the relationship, will be used as the name of the relationship created in the graph
	 *
	 * @static
	 * @member {string}
	 */
	static get type()
	{
		return this[_type] || this.DEFAULT_TYPE
	}

	static set type(value)
	{
		/**
		 * @private
		 */
		this[_type] = value
	}

	/**
	 * If true a relationship will always have only a single related Node
	 *
	 * @static
	 * @member {boolean}
	 */
	static get singular()
	{
		return this[_singular] === true
	}

	static set singular(value)
	{
		/**
		 * @private
		 */
		this[_singular] = !!value
	}

	/**
	 * The direction of the relationship, can be {@link Relationship.IN} or {@link Relationship.OUT}
	 *
	 * @static
	 * @member {string}
	 */
	static get direction()
	{
		return this[_direction] || this.OUT
	}

	static set direction(value)
	{
		/**
		 * @private
		*/
		this[_direction] = value
	}

	/**
	 * The related Node class or a function that returns a Node class
	 *
	 * @static
	 * @member {Node|function}
	 */
	static get Model()
	{
		if (typeof this[_model] === 'function' && !(this[_model].prototype instanceof Model))
		{
			return this[_model]()
		}

		return this[_model] || require('./Node')
	}

	static set Model(Class)
	{
		/**
		 * @private
		*/
		this[_model] = Class
	}

	/**
	 * Alias for Model
	 *
	 * @static
	 * @member {Node|function}
	 */
	static get model()
	{
		return this.Model
	}

	static set model(Class)
	{
		this.Model = Class
	}
}

module.exports = Relationship