Home Manual Reference Source Test

src/lib/model/opcua/NodeId.js

import { sep } from 'path';
import { NodeId as OpcNodeId } from 'node-opcua/lib/datamodel/nodeid';

/**
 * OPC-UA node id types.
 * @type {Map<String, node-opcua~NodeIdType>}
 */
const Type = OpcNodeId.NodeIdType;

/**
 * OPC-UA node id types mapped against node-id identifiers (e.g. i, s ...).
 * @type {Map<String, node-opcua~NodeIdType>}
 */
const TypeForIdentifier = {
  i: Type.NUMERIC,
  s: Type.STRING,
  g: Type.GUID,
  b: Type.BYTESTRING,
};

/**
 * Resource nodes are only allowed to have these child nodes.
 * @type {Set<string>}
 */
const possibleResourceChildNodes = new Set(['Translate', 'Compress']);

/**
 * A wrapper around {@link node-opcua~NodeId}.
 */
export default class NodeId extends OpcNodeId {
  /**
   * Creates a new NodeId. Can be called in multiple ways:
   *  - with a {@link node-opcua~NodeIdType}, a value and a namespace (defaults to 0),
   *  - with a value only (type will be taken from it, namespace defaults to 1) or
   *  - with a {@link NodeId}s string representation (for example `ns=1;s=AGENT.DISPLAYS`).
   * @param {node-opcua~NodeIdType|string|number} typeOrValue The type or value to use.
   * @param {(number|string)} [value] The value to use.
   * @param {number} [namespace=1] The namespace to use.
   */
  constructor(typeOrValue, value, namespace = 1) {
    if (!Type.get(typeOrValue)) {
      let m = null;

      if (typeof typeOrValue === 'string') {
        m = typeOrValue.match(/^ns=([0-9]+);(i|s|g|b)=(.*)$/);
      }

      if (m === null) {
        super(
          Number.isNaN(Number.parseInt(typeOrValue, 10)) ? Type.STRING : Type.NUMERIC,
          typeOrValue,
          1
        );
      } else {
        const n = Number.parseInt(m[1], 10);
        const t = TypeForIdentifier[m[2]];
        const v = t === Type.NUMERIC ? Number.parseInt(m[3], 10) : m[3];

        super(t, v, n);
      }
    } else {
      super(typeOrValue, value, namespace);
    }
  }

  /**
   * Creates a new NodeId based on a file path.
   * @param {string} path The file path to use.
   * @return {NodeId} The resulting NodeId.
   */
  static fromFilePath(path) {
    let separator = '.';

    const value = path.split(sep).reduce((result, current, index, components) => {
      const next = `${result ? `${result}${separator}` : ''}${current.replace('%2F', '/')}`;

      if (current === 'RESOURCES') {
        separator = '/';
      } else if (separator === '/' && possibleResourceChildNodes.has(components[index + 1])) {
        separator = '.';
      }

      return next;
    }, '');

    return new NodeId(NodeId.NodeIdType.STRING, value, 1);
  }

  /**
   * The node id's value, encoded to a file path.
   * @type {string}
   */
  get filePath() {
    const parts = this.value.split('RESOURCES');
    parts[0] = parts[0].replace('/', '%2F').split('.').join('/');

    return parts.join('RESOURCES');
  }

  // eslint-disable-next-line jsdoc/require-description-complete-sentence
  /**
   * Returns the last separator in a string node id's path, e.g.:
   * - `'/'` for `ns=1;SYSTEM.LIBRARY.RESOURCES/index.htm`,
   * - `'.'` for `ns=1;AGENT.DISPLAYS.Main`.
   * @type {?string} `null` for non-string node ids, `'/'` for resource paths, `'.'` for regular
   * string node ids.
   */
  get _lastSeparator() {
    if (this.identifierType !== NodeId.NodeIdType.STRING) {
      return null;
    }

    return ~this.value.indexOf('/') ? '/' : '.';
  }

  /**
   * The parent node id, or `null`.
   * @type {?NodeId}
   * @deprecated Doesn't work properly in some edge cases. Use AtviseFile#parentNodeId instead
   * whenever possible.
   */
  get parent() {
    if (this.identifierType !== NodeId.NodeIdType.STRING) {
      return null;
    }

    /*
      Known aliases:
        - AGENT and SYSTEM are children of "Objects"
        - ObjectTypes.PROJECT and VariableTypes.PROJECT are children of their base Types
    */
    // FIXME: Should be in mapping transformer
    if (this.value === 'AGENT' || this.value === 'SYSTEM') {
      return new NodeId(NodeId.NodeIdType.NUMERIC, 85, 0); // "Objects"
    } else if (this.value === 'ObjectTypes.PROJECT') {
      return new NodeId(NodeId.NodeIdType.NUMERIC, 58, 0); // "BaseObjectType"
    } else if (this.value === 'VariableTypes.PROJECT') {
      return new NodeId(NodeId.NodeIdType.NUMERIC, 62, 0); // "BaseVariableType"
    }

    const parentValue = this.value.substr(0, this.value.lastIndexOf(this._lastSeparator));

    if (!parentValue) {
      // Root node -> 'Objects' is parent
      return new NodeId(NodeId.NodeIdType.NUMERIC, 85, 0);
    }

    return new NodeId(NodeId.NodeIdType.STRING, parentValue, this.namespace);
  }

  /**
   * Checks if the node is a child of another.
   * @param {NodeId} parent The possible parent to check.
   * @return {boolean} `true` if *this* is a child node of *parent*.
   */
  isChildOf(parent) {
    if (
      this.identifierType !== NodeId.NodeIdType.STRING ||
      parent.identifierType !== NodeId.NodeIdType.STRING
    ) {
      return false;
    }

    if (this.namespace !== parent.namespace || this.value === parent.value) {
      return false;
    }

    const [prefix, postfix] = this.value.split(parent.value);

    return (
      prefix === '' &&
      postfix &&
      (postfix[0] === this._lastSeparator ||
        (this._lastSeparator === '/' && postfix[0] === '.' && postfix.split('.').length === 2))
    );
  }

  /**
   * The node id's browsename as string.
   * @type {string}
   */
  get browseName() {
    if (this.identifierType !== NodeId.NodeIdType.STRING) {
      return null;
    }

    return this.value.substr(this.value.lastIndexOf(this._lastSeparator) + 1);
  }

  /**
   * Returns a string in the format "namespace value" that is printed when inspecting the NodeId
   * using {@link util~inspect}.
   * @see https://nodejs.org/api/util.html#util_util_inspect_object_options
   * @param {number} depth The depth to inspect.
   * @param {Object} options The options to use.
   * @return {string} A string in the format "namespace value".
   */
  inspect(depth, options) {
    return [
      options.stylize(this.namespace, 'number'),
      options.stylize(this.value, this.identifierType === Type.NUMERIC ? 'number' : 'string'),
    ].join(' ');
  }
}