Home Manual Reference Source Test

src/lib/model/Node.ts

import { ReferenceTypeIds as OpcReferenceTypeIds } from 'node-opcua/lib/opcua_node_ids';
import { NodeClass } from 'node-opcua/lib/datamodel/nodeclass';
import { VariantArrayType, DataType, Variant } from 'node-opcua/lib/datamodel/variant';
import { ItemOf, KeyOf } from 'node-opcua/lib/misc/enum.js';
import { reverse } from '../helpers/Object';
import { sortReferences } from '../helpers/mapping';
import { ValueOf } from '../helpers/types';

/**
 * References type ids.
 */
export const ReferenceTypeIds = {
  ...OpcReferenceTypeIds,
  toParent: -1,
};

/** A reference type name */
type ReferenceTypeName = keyof typeof ReferenceTypeIds;

/** A raw (number) reference type */
type ReferenceType = ValueOf<typeof ReferenceTypeIds>;

/** Node references stored in definition files */
export type ReferenceDefinitions = {
  [type in ReferenceTypeName]?: (number | string)[];
};

/** Node definition stored in definition file */
export interface NodeDefinition {
  nodeId?: string;
  nodeClass?: KeyOf<typeof NodeClass>; // Defaults to 'Variable'
  dataType?: KeyOf<typeof DataType>;
  arrayType?: KeyOf<typeof VariantArrayType>;
  references?: ReferenceDefinitions;
}

/**
 * Names for references.
 */
export const ReferenceTypeNames = reverse(ReferenceTypeIds) as { [key: number]: string };

/**
 * A map specialized for holding references.
 */
class ReferenceMap extends Map<ReferenceType, Set<number | string>> {
  /**
   * Adds a new reference.
   * @param {number} type The reference id.
   * @param {string} nodeId The reference target node's id.
   */
  public addReference(type: ReferenceType, nodeId: number | string): void {
    const set = this.get(type);
    if (set) {
      set.add(nodeId);
    } else {
      this.set(type, new Set([nodeId]));
    }
  }

  /**
   * Removes the given reference.
   * @param {number} type The reference id.
   * @param {string} nodeId The reference target node's id.
   */
  public deleteReference(type: ReferenceType, nodeId: number | string): number | string {
    const set = this.get(type);
    if (set) {
      const ref = set.delete(nodeId);

      if (ref) {
        if (set.size === 0) {
          this.delete(type);
        }

        return nodeId;
      }
    }

    throw new Error(`No ${ReferenceTypeNames[type] || type} reference to ${nodeId}`);
  }

  /**
   * Returns the first entry of a specific type.
   * @param type The reference type id to look for.
   * @return The first reference found or undefined.
   */
  public getSingle(type: ReferenceType): number | string | undefined {
    const set = this.get(type);
    return set && Array.from(set)[0];
  }

  /**
   * Returns a plain object of refernces.
   * @return A string describing the reference map.
   */
  public toJSON(): ReferenceDefinitions {
    return [...this].reduce(
      (result, [key, value]) =>
        Object.assign(result, {
          [ReferenceTypeNames[key] || key]: [...value],
        }),
      {}
    );
  }
}

interface WithValue {
  value: Variant;
}

export interface NodeOptions {
  name: string;
  parent?: Node;
  nodeClass: ItemOf<typeof NodeClass>;
}

type NodeResolveKey = 'nodeClass' | 'dataType' | 'arrayType';

/**
 * The main model class.
 */
export default abstract class Node {
  /** The node's name when stored to a file. */
  protected fileName: string;
  /** The node's name when written to the server. */
  protected idName: string;
  /** The id stored in the definition file. */
  protected specialId?: string;

  /** The node's parent node. */
  public readonly parent?: Node;
  /** The node's class. */
  public readonly nodeClass: ItemOf<typeof NodeClass>;

  /** A set of resolved properties. */
  protected _resolved = new Set<NodeResolveKey>();
  /** A set of unresolved properties. */
  protected _unresolved: Set<NodeResolveKey>;
  /** The node's references. */
  public references = new ReferenceMap();
  /** The node's unresolved refernces. */
  protected _resolvedReferences = new ReferenceMap();
  /** The node's resolved refernces. */
  protected _unresolvedReferences = new ReferenceMap();
  /** If the parent node resolves metadata. */
  protected _parentResolvesMetadata = false;

  /**
   * Creates a new node.
   * @param {Object} options The options to use.
   * @param {string} options.name The node's name.
   * @param {Node} options.parent The node's parent node.
   * @param {node-opcua~NodeClass} options.nodeClass The node's class.
   */
  public constructor({ name, parent, nodeClass /* , referenceToParent */ }: NodeOptions) {
    this.fileName = name;
    this.idName = name;
    this.parent = parent;
    this.nodeClass = nodeClass;

    this._unresolved = new Set([
      'nodeClass',
      // Only for variables
      'dataType',
      'arrayType',
    ]);
  }

  /**
   * If the parent resolves metadata (for example: split transformer source files).
   */
  public get parentResolvesMetadata(): boolean {
    return this._parentResolvesMetadata;
  }

  public markAsResolved(key: NodeResolveKey): void {
    const value = this._unresolved.delete(key);

    // FIXME: Only test if debug / test
    if (value === false) {
      throw new Error(`'${key}' is already resolved`);
    }

    this._resolved.add(key);
  }

  public isResolved(key: NodeResolveKey): boolean {
    return this._resolved.has(key);
  }

  /**
   * Adds a new reference.
   * @param {number} type The reference type's id.
   * @param {string} id The reference target node's id.
   */
  public addReference(type: ReferenceType, id: string): void {
    this.references.addReference(type, id);
    this._unresolvedReferences.addReference(type, id);
  }

  public setReferences(type: ReferenceType, ids: string[]): void {
    this.references.set(type, new Set(ids));
    this._unresolvedReferences.set(type, new Set(ids));
  }

  public markReferenceAsResolved(name: ReferenceTypeName, value: string): void {
    const type = ReferenceTypeIds[name];
    const ref = this._unresolvedReferences.deleteReference(type, value);
    this._resolvedReferences.addReference(type, ref);
  }

  public markAllReferencesAsResolved(name: ReferenceTypeName): void {
    const type = ReferenceTypeIds[name];
    this._unresolvedReferences.delete(type);
  }

  public hasUnresolvedReference(name: ReferenceTypeName): boolean {
    const type = ReferenceTypeIds[name];
    return this._unresolvedReferences.has(type);
  }

  /**
   * The node's file path, used to compute {@link Node#filePath}.
   */
  private get _filePath(): string[] {
    if (!this.parent) {
      return [this.fileName];
    }
    return this.parent._filePath.concat(this.fileName);
  }

  /**
   * The node's file path.
   */
  public get filePath(): string[] {
    if (!this.parent) {
      return [];
    }
    return this.parent._filePath;
  }

  /**
   * The node's id, used to compute {@link Node#nodeId}.
   */
  private get _nodeId(): { id: string; separator: '/' | '.' } {
    if (this.specialId) {
      return {
        id: this.specialId,
        separator: this.specialId.match(/\.RESOURCES\/?/) ? '/' : '.',
      };
    }

    if (!this.parent) {
      return {
        id: this.idName,
        separator: '.',
      };
    }

    const { separator, id } = this.parent._nodeId;

    if (this._parentResolvesMetadata) {
      return { separator, id };
    }

    return {
      separator: this.idName === 'RESOURCES' ? '/' : separator,
      id: `${id}${separator}${this.idName}`,
    };
  }

  /**
   * The node's id.
   */
  public get nodeId(): string {
    return this._nodeId.id;
  }

  /**
   * The node's type definition if given.
   */
  public get typeDefinition(): number | string | undefined {
    return this.references.getSingle(ReferenceTypeIds.HasTypeDefinition);
  }

  /**
   * The node's modellingRule if given.
   * @type {?number}
   */
  public get modellingRule(): number | string | undefined {
    return this.references.getSingle(ReferenceTypeIds.HasModellingRule);
  }

  /**
   * Returns `true` if the node has the given type definition.
   * @param typeDefName - The type definition to check.
   * @return If the node has the given type definition.
   */
  public hasTypeDefinition(typeDefName: number | string): boolean {
    const def = this.typeDefinition;

    return def ? def === typeDefName : false;
  }

  /**
   * `true` at the moment.
   */
  public get hasUnresolvedMetadata(): boolean {
    return true;
    /* FIXME: Once plugin mapping is implemented
    const value = !this._parentResolvesMetadata && (Boolean(this._unresolved.size) ||
      Boolean(this._unresolvedReferences.size) || this.specialId);

    // FIXME: If debug / test
    if (!value && Object.keys(this.metadata).length > 0) {
      throw new Error(`#hasUnresolvedMetadata did return invalid result ${
        value
      } for ${
        JSON.stringify(Object.assign(this, {parent: undefined, value: undefined }), null, '  ')
      }`);
    } else if (value && Object.keys(this.metadata).length === 0) {
      throw new Error('#metadata did return invalid result');
    }

    return value; */
  }

  /**
   * The metadata to store in the node's definition file.
   * @type {Object}
   */
  public get metadata(): NodeDefinition {
    if (this._parentResolvesMetadata) {
      return {};
    }

    const meta: Partial<NodeDefinition> = {};

    if (this.specialId) {
      meta.nodeId = this.specialId;
    }

    if (this.isVariableNode()) {
      meta.dataType = this.value.dataType.key;
      meta.arrayType = this.value.arrayType.key;
    } else {
      meta.nodeClass = this.nodeClass.key;
    }

    meta.references = sortReferences(this.references.toJSON());

    /* FIXME: Once plugin mapping is implemented
    for (const unresolved of this._unresolved) {
      let value = this[unresolved];

      if (unresolved === 'dataType') {
        value = this.value.dataType ? this.value.dataType.key : 'UNKNOWN';
      } else if (unresolved === 'arrayType') {
        value = this.value.arrayType ? this.value.arrayType.key : 'UNKNOWN';
      }

      meta[unresolved] = value;
    }


    if (this._unresolvedReferences.size) {
      meta.references = sortReferences(this._unresolvedReferences.toJSON());
    }
    */

    return meta;
  }

  // Manipulation

  /**
   * Creates a new child node.
   * @param {Object} options The options to use.
   * @param {string} options.extension The extension to append to the node's name.
   */
  public createChild({ extension }: { extension: string }): Node {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const node: Node = new (this.constructor as any)({
      name: this.idName,
      parent: this,
      nodeClass: this.nodeClass,
    });

    node.fileName = `${this.fileName}${extension}`;

    node.references = this.references;
    node._parentResolvesMetadata = true;

    return node;
  }

  // Convenience getters

  /**
   * The node's data type.
   */
  public get dataType(): ItemOf<typeof DataType> {
    if (!this.isVariableNode()) {
      throw new TypeError('Not a variable node');
    }

    return this.value.dataType;
  }

  /**
   * The node's array type.
   */
  public get arrayType(): ItemOf<typeof VariantArrayType> {
    if (!this.isVariableNode()) {
      throw new TypeError('Not a variable node');
    }

    return this.value.arrayType;
  }

  /**
   * If the node is a variable.
   * @deprecated Use TypeScript compatible {@link Node#isVariableNode} instead.
   */
  public get isVariable(): boolean {
    return this.nodeClass === NodeClass.Variable;
  }

  public isVariableNode(): this is WithValue {
    return this.isVariable;
  }

  // FIXME: Move to display / script transformers

  /**
   * If the node is an object display.
   */
  public get isDisplay(): boolean {
    return this.hasTypeDefinition('VariableTypes.ATVISE.Display');
  }

  /**
   * If the node is a serverside script.
   */
  public get isScript(): boolean {
    return this.hasTypeDefinition('VariableTypes.ATVISE.ScriptCode');
  }

  /**
   * If the node is a quickdynamic.
   */
  public get isQuickDynamic(): boolean {
    return this.hasTypeDefinition('VariableTypes.ATVISE.QuickDynamic');
  }

  /**
   * If the node is a display script.
   */
  public get isDisplayScript(): boolean {
    return this.hasTypeDefinition('VariableTypes.ATVISE.DisplayScript');
  }
}

/**
 * A node during a *pull*.
 */
export abstract class ServerNode extends Node {
  /**
   * The node's name.
   */
  public get name(): string {
    return this.fileName;
  }

  /**
   * Renames a node.
   * @param name The name to set.
   */
  public renameTo(name: string): void {
    this.fileName = name;
  }
}

/**
 * A node during a *push*.
 */
export abstract class SourceNode extends Node {
  /**
   * The node's name.
   */
  public get name(): string {
    return this.idName;
  }

  /**
   * Renames a node.
   * @param name The name to set.
   */
  public renameTo(name: string): void {
    this.idName = name;
  }
}