Home Manual Reference Source Test

src/lib/model/Node.ts

  1. import { ReferenceTypeIds as OpcReferenceTypeIds } from 'node-opcua/lib/opcua_node_ids';
  2. import { NodeClass } from 'node-opcua/lib/datamodel/nodeclass';
  3. import { VariantArrayType, DataType, Variant } from 'node-opcua/lib/datamodel/variant';
  4. import { ItemOf, KeyOf } from 'node-opcua/lib/misc/enum.js';
  5. import { reverse } from '../helpers/Object';
  6. import { sortReferences } from '../helpers/mapping';
  7. import { ValueOf } from '../helpers/types';
  8.  
  9. /**
  10. * References type ids.
  11. */
  12. export const ReferenceTypeIds = {
  13. ...OpcReferenceTypeIds,
  14. toParent: -1,
  15. };
  16.  
  17. /** A reference type name */
  18. type ReferenceTypeName = keyof typeof ReferenceTypeIds;
  19.  
  20. /** A raw (number) reference type */
  21. type ReferenceType = ValueOf<typeof ReferenceTypeIds>;
  22.  
  23. /** Node references stored in definition files */
  24. export type ReferenceDefinitions = {
  25. [type in ReferenceTypeName]?: (number | string)[];
  26. };
  27.  
  28. /** Node definition stored in definition file */
  29. export interface NodeDefinition {
  30. nodeId?: string;
  31. nodeClass?: KeyOf<typeof NodeClass>; // Defaults to 'Variable'
  32. dataType?: KeyOf<typeof DataType>;
  33. arrayType?: KeyOf<typeof VariantArrayType>;
  34. references?: ReferenceDefinitions;
  35. }
  36.  
  37. /**
  38. * Names for references.
  39. */
  40. export const ReferenceTypeNames = reverse(ReferenceTypeIds) as { [key: number]: string };
  41.  
  42. /**
  43. * A map specialized for holding references.
  44. */
  45. class ReferenceMap extends Map<ReferenceType, Set<number | string>> {
  46. /**
  47. * Adds a new reference.
  48. * @param {number} type The reference id.
  49. * @param {string} nodeId The reference target node's id.
  50. */
  51. public addReference(type: ReferenceType, nodeId: number | string): void {
  52. const set = this.get(type);
  53. if (set) {
  54. set.add(nodeId);
  55. } else {
  56. this.set(type, new Set([nodeId]));
  57. }
  58. }
  59.  
  60. /**
  61. * Removes the given reference.
  62. * @param {number} type The reference id.
  63. * @param {string} nodeId The reference target node's id.
  64. */
  65. public deleteReference(type: ReferenceType, nodeId: number | string): number | string {
  66. const set = this.get(type);
  67. if (set) {
  68. const ref = set.delete(nodeId);
  69.  
  70. if (ref) {
  71. if (set.size === 0) {
  72. this.delete(type);
  73. }
  74.  
  75. return nodeId;
  76. }
  77. }
  78.  
  79. throw new Error(`No ${ReferenceTypeNames[type] || type} reference to ${nodeId}`);
  80. }
  81.  
  82. /**
  83. * Returns the first entry of a specific type.
  84. * @param type The reference type id to look for.
  85. * @return The first reference found or undefined.
  86. */
  87. public getSingle(type: ReferenceType): number | string | undefined {
  88. const set = this.get(type);
  89. return set && Array.from(set)[0];
  90. }
  91.  
  92. /**
  93. * Returns a plain object of refernces.
  94. * @return A string describing the reference map.
  95. */
  96. public toJSON(): ReferenceDefinitions {
  97. return [...this].reduce(
  98. (result, [key, value]) =>
  99. Object.assign(result, {
  100. [ReferenceTypeNames[key] || key]: [...value],
  101. }),
  102. {}
  103. );
  104. }
  105. }
  106.  
  107. interface WithValue {
  108. value: Variant;
  109. }
  110.  
  111. export interface NodeOptions {
  112. name: string;
  113. parent?: Node;
  114. nodeClass: ItemOf<typeof NodeClass>;
  115. }
  116.  
  117. type NodeResolveKey = 'nodeClass' | 'dataType' | 'arrayType';
  118.  
  119. /**
  120. * The main model class.
  121. */
  122. export default abstract class Node {
  123. /** The node's name when stored to a file. */
  124. protected fileName: string;
  125. /** The node's name when written to the server. */
  126. protected idName: string;
  127. /** The id stored in the definition file. */
  128. protected specialId?: string;
  129.  
  130. /** The node's parent node. */
  131. public readonly parent?: Node;
  132. /** The node's class. */
  133. public readonly nodeClass: ItemOf<typeof NodeClass>;
  134.  
  135. /** A set of resolved properties. */
  136. protected _resolved = new Set<NodeResolveKey>();
  137. /** A set of unresolved properties. */
  138. protected _unresolved: Set<NodeResolveKey>;
  139. /** The node's references. */
  140. public references = new ReferenceMap();
  141. /** The node's unresolved refernces. */
  142. protected _resolvedReferences = new ReferenceMap();
  143. /** The node's resolved refernces. */
  144. protected _unresolvedReferences = new ReferenceMap();
  145. /** If the parent node resolves metadata. */
  146. protected _parentResolvesMetadata = false;
  147.  
  148. /**
  149. * Creates a new node.
  150. * @param {Object} options The options to use.
  151. * @param {string} options.name The node's name.
  152. * @param {Node} options.parent The node's parent node.
  153. * @param {node-opcua~NodeClass} options.nodeClass The node's class.
  154. */
  155. public constructor({ name, parent, nodeClass /* , referenceToParent */ }: NodeOptions) {
  156. this.fileName = name;
  157. this.idName = name;
  158. this.parent = parent;
  159. this.nodeClass = nodeClass;
  160.  
  161. this._unresolved = new Set([
  162. 'nodeClass',
  163. // Only for variables
  164. 'dataType',
  165. 'arrayType',
  166. ]);
  167. }
  168.  
  169. /**
  170. * If the parent resolves metadata (for example: split transformer source files).
  171. */
  172. public get parentResolvesMetadata(): boolean {
  173. return this._parentResolvesMetadata;
  174. }
  175.  
  176. public markAsResolved(key: NodeResolveKey): void {
  177. const value = this._unresolved.delete(key);
  178.  
  179. // FIXME: Only test if debug / test
  180. if (value === false) {
  181. throw new Error(`'${key}' is already resolved`);
  182. }
  183.  
  184. this._resolved.add(key);
  185. }
  186.  
  187. public isResolved(key: NodeResolveKey): boolean {
  188. return this._resolved.has(key);
  189. }
  190.  
  191. /**
  192. * Adds a new reference.
  193. * @param {number} type The reference type's id.
  194. * @param {string} id The reference target node's id.
  195. */
  196. public addReference(type: ReferenceType, id: string): void {
  197. this.references.addReference(type, id);
  198. this._unresolvedReferences.addReference(type, id);
  199. }
  200.  
  201. public setReferences(type: ReferenceType, ids: string[]): void {
  202. this.references.set(type, new Set(ids));
  203. this._unresolvedReferences.set(type, new Set(ids));
  204. }
  205.  
  206. public markReferenceAsResolved(name: ReferenceTypeName, value: string): void {
  207. const type = ReferenceTypeIds[name];
  208. const ref = this._unresolvedReferences.deleteReference(type, value);
  209. this._resolvedReferences.addReference(type, ref);
  210. }
  211.  
  212. public markAllReferencesAsResolved(name: ReferenceTypeName): void {
  213. const type = ReferenceTypeIds[name];
  214. this._unresolvedReferences.delete(type);
  215. }
  216.  
  217. public hasUnresolvedReference(name: ReferenceTypeName): boolean {
  218. const type = ReferenceTypeIds[name];
  219. return this._unresolvedReferences.has(type);
  220. }
  221.  
  222. /**
  223. * The node's file path, used to compute {@link Node#filePath}.
  224. */
  225. private get _filePath(): string[] {
  226. if (!this.parent) {
  227. return [this.fileName];
  228. }
  229. return this.parent._filePath.concat(this.fileName);
  230. }
  231.  
  232. /**
  233. * The node's file path.
  234. */
  235. public get filePath(): string[] {
  236. if (!this.parent) {
  237. return [];
  238. }
  239. return this.parent._filePath;
  240. }
  241.  
  242. /**
  243. * The node's id, used to compute {@link Node#nodeId}.
  244. */
  245. private get _nodeId(): { id: string; separator: '/' | '.' } {
  246. if (this.specialId) {
  247. return {
  248. id: this.specialId,
  249. separator: this.specialId.match(/\.RESOURCES\/?/) ? '/' : '.',
  250. };
  251. }
  252.  
  253. if (!this.parent) {
  254. return {
  255. id: this.idName,
  256. separator: '.',
  257. };
  258. }
  259.  
  260. const { separator, id } = this.parent._nodeId;
  261.  
  262. if (this._parentResolvesMetadata) {
  263. return { separator, id };
  264. }
  265.  
  266. return {
  267. separator: this.idName === 'RESOURCES' ? '/' : separator,
  268. id: `${id}${separator}${this.idName}`,
  269. };
  270. }
  271.  
  272. /**
  273. * The node's id.
  274. */
  275. public get nodeId(): string {
  276. return this._nodeId.id;
  277. }
  278.  
  279. /**
  280. * The node's type definition if given.
  281. */
  282. public get typeDefinition(): number | string | undefined {
  283. return this.references.getSingle(ReferenceTypeIds.HasTypeDefinition);
  284. }
  285.  
  286. /**
  287. * The node's modellingRule if given.
  288. * @type {?number}
  289. */
  290. public get modellingRule(): number | string | undefined {
  291. return this.references.getSingle(ReferenceTypeIds.HasModellingRule);
  292. }
  293.  
  294. /**
  295. * Returns `true` if the node has the given type definition.
  296. * @param typeDefName - The type definition to check.
  297. * @return If the node has the given type definition.
  298. */
  299. public hasTypeDefinition(typeDefName: number | string): boolean {
  300. const def = this.typeDefinition;
  301.  
  302. return def ? def === typeDefName : false;
  303. }
  304.  
  305. /**
  306. * `true` at the moment.
  307. */
  308. public get hasUnresolvedMetadata(): boolean {
  309. return true;
  310. /* FIXME: Once plugin mapping is implemented
  311. const value = !this._parentResolvesMetadata && (Boolean(this._unresolved.size) ||
  312. Boolean(this._unresolvedReferences.size) || this.specialId);
  313.  
  314. // FIXME: If debug / test
  315. if (!value && Object.keys(this.metadata).length > 0) {
  316. throw new Error(`#hasUnresolvedMetadata did return invalid result ${
  317. value
  318. } for ${
  319. JSON.stringify(Object.assign(this, {parent: undefined, value: undefined }), null, ' ')
  320. }`);
  321. } else if (value && Object.keys(this.metadata).length === 0) {
  322. throw new Error('#metadata did return invalid result');
  323. }
  324.  
  325. return value; */
  326. }
  327.  
  328. /**
  329. * The metadata to store in the node's definition file.
  330. * @type {Object}
  331. */
  332. public get metadata(): NodeDefinition {
  333. if (this._parentResolvesMetadata) {
  334. return {};
  335. }
  336.  
  337. const meta: Partial<NodeDefinition> = {};
  338.  
  339. if (this.specialId) {
  340. meta.nodeId = this.specialId;
  341. }
  342.  
  343. if (this.isVariableNode()) {
  344. meta.dataType = this.value.dataType.key;
  345. meta.arrayType = this.value.arrayType.key;
  346. } else {
  347. meta.nodeClass = this.nodeClass.key;
  348. }
  349.  
  350. meta.references = sortReferences(this.references.toJSON());
  351.  
  352. /* FIXME: Once plugin mapping is implemented
  353. for (const unresolved of this._unresolved) {
  354. let value = this[unresolved];
  355.  
  356. if (unresolved === 'dataType') {
  357. value = this.value.dataType ? this.value.dataType.key : 'UNKNOWN';
  358. } else if (unresolved === 'arrayType') {
  359. value = this.value.arrayType ? this.value.arrayType.key : 'UNKNOWN';
  360. }
  361.  
  362. meta[unresolved] = value;
  363. }
  364.  
  365.  
  366. if (this._unresolvedReferences.size) {
  367. meta.references = sortReferences(this._unresolvedReferences.toJSON());
  368. }
  369. */
  370.  
  371. return meta;
  372. }
  373.  
  374. // Manipulation
  375.  
  376. /**
  377. * Creates a new child node.
  378. * @param {Object} options The options to use.
  379. * @param {string} options.extension The extension to append to the node's name.
  380. */
  381. public createChild({ extension }: { extension: string }): Node {
  382. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  383. const node: Node = new (this.constructor as any)({
  384. name: this.idName,
  385. parent: this,
  386. nodeClass: this.nodeClass,
  387. });
  388.  
  389. node.fileName = `${this.fileName}${extension}`;
  390.  
  391. node.references = this.references;
  392. node._parentResolvesMetadata = true;
  393.  
  394. return node;
  395. }
  396.  
  397. // Convenience getters
  398.  
  399. /**
  400. * The node's data type.
  401. */
  402. public get dataType(): ItemOf<typeof DataType> {
  403. if (!this.isVariableNode()) {
  404. throw new TypeError('Not a variable node');
  405. }
  406.  
  407. return this.value.dataType;
  408. }
  409.  
  410. /**
  411. * The node's array type.
  412. */
  413. public get arrayType(): ItemOf<typeof VariantArrayType> {
  414. if (!this.isVariableNode()) {
  415. throw new TypeError('Not a variable node');
  416. }
  417.  
  418. return this.value.arrayType;
  419. }
  420.  
  421. /**
  422. * If the node is a variable.
  423. * @deprecated Use TypeScript compatible {@link Node#isVariableNode} instead.
  424. */
  425. public get isVariable(): boolean {
  426. return this.nodeClass === NodeClass.Variable;
  427. }
  428.  
  429. public isVariableNode(): this is WithValue {
  430. return this.isVariable;
  431. }
  432.  
  433. // FIXME: Move to display / script transformers
  434.  
  435. /**
  436. * If the node is an object display.
  437. */
  438. public get isDisplay(): boolean {
  439. return this.hasTypeDefinition('VariableTypes.ATVISE.Display');
  440. }
  441.  
  442. /**
  443. * If the node is a serverside script.
  444. */
  445. public get isScript(): boolean {
  446. return this.hasTypeDefinition('VariableTypes.ATVISE.ScriptCode');
  447. }
  448.  
  449. /**
  450. * If the node is a quickdynamic.
  451. */
  452. public get isQuickDynamic(): boolean {
  453. return this.hasTypeDefinition('VariableTypes.ATVISE.QuickDynamic');
  454. }
  455.  
  456. /**
  457. * If the node is a display script.
  458. */
  459. public get isDisplayScript(): boolean {
  460. return this.hasTypeDefinition('VariableTypes.ATVISE.DisplayScript');
  461. }
  462. }
  463.  
  464. /**
  465. * A node during a *pull*.
  466. */
  467. export abstract class ServerNode extends Node {
  468. /**
  469. * The node's name.
  470. */
  471. public get name(): string {
  472. return this.fileName;
  473. }
  474.  
  475. /**
  476. * Renames a node.
  477. * @param name The name to set.
  478. */
  479. public renameTo(name: string): void {
  480. this.fileName = name;
  481. }
  482. }
  483.  
  484. /**
  485. * A node during a *push*.
  486. */
  487. export abstract class SourceNode extends Node {
  488. /**
  489. * The node's name.
  490. */
  491. public get name(): string {
  492. return this.idName;
  493. }
  494.  
  495. /**
  496. * Renames a node.
  497. * @param name The name to set.
  498. */
  499. public renameTo(name: string): void {
  500. this.idName = name;
  501. }
  502. }