node-opcua-uanodeset-rdf 2.182.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # node-opcua-uanodeset-rdf
2
+
3
+ Export an OPC UA information model as **JSON-LD**, so it can be queried with SPARQL, reasoned
4
+ over with an OWL toolchain, or joined to data that was never OPC UA in the first place.
5
+
6
+ see http://node-opcua.github.io/
7
+
8
+ ```bash
9
+ npm install node-opcua-uanodeset-rdf
10
+ ```
11
+
12
+ ## Why
13
+
14
+ An address space is already a graph: nodes, typed edges, subtype chains. What it lacks is a way
15
+ to ask graph questions of it. `HasComponent` is a traversal in an SDK, but a *predicate* in RDF,
16
+ and once the model is in a triple store you can ask things the OPC UA services have no method
17
+ for:
18
+
19
+ > every DataType in this model with a field whose own DataType is not defined by any model I
20
+ > imported
21
+
22
+ > the transitive closure of `HasSubtype` under `DeviceType`, across DI and a vendor companion
23
+ > specification loaded side by side
24
+
25
+ > which nodes changed between two published versions of a companion specification
26
+
27
+ That is the job. Get the model once, export it, and let a graph database answer.
28
+
29
+ ## What comes out
30
+
31
+ ```jsonc
32
+ {
33
+ "@context": { /* prefixes, property terms, and per-document predicates */ },
34
+ "@graph": [ /* the ontology node, then one entry per node */ ],
35
+ "@included": [ /* OWL property declarations, and foreign attachments */ ]
36
+ }
37
+ ```
38
+
39
+ The first `@graph` entry describes the model itself, as both a `uardf:UANodeSet` and an
40
+ `owl:Ontology`. Every entry after it is a node:
41
+
42
+ ```jsonc
43
+ {
44
+ "@id": "di:DeviceType",
45
+ "@type": ["uardf:UAObjectType", "owl:Class"],
46
+ "nodeId": "nsu=http://opcfoundation.org/UA/DI/;i=1002",
47
+ "browseName": "nsu=http://opcfoundation.org/UA/DI/;DeviceType",
48
+ "name": "DeviceType",
49
+ "subClassOf": "di:ComponentType",
50
+ "di:ParameterSet": "di:bnN1PWh0dHA6..."
51
+ }
52
+ ```
53
+
54
+ Three ideas carry most of the mapping:
55
+
56
+ - **Types get readable IRIs, instances get stable ones.** An ObjectType, VariableType,
57
+ ReferenceType or DataType is `<prefix>:<BrowseName>`; everything else is
58
+ `<prefix>:<base64url(canonical NodeId)>`, unpadded. So `di:DeviceType` is legible in a query
59
+ and an instance IRI survives a renaming.
60
+ - **Three references become fields rather than edges.** `HasTypeDefinition` becomes part of
61
+ `@type` (a Property is `["uardf:UAVariable", "opcua:PropertyType"]`), `HasSubtype` becomes
62
+ `subClassOf`, and `HasModellingRule` becomes `modellingRule`. That is what makes the OWL
63
+ reasoning work: a subtype chain is a class hierarchy.
64
+ - **`@included` carries the schema.** One `owl:ObjectProperty` declaration per reference type the
65
+ model defines, with its inverse and its `subPropertyOf`, so a reasoner knows what the
66
+ predicates mean.
67
+
68
+ The legacy OPC Binary machinery is dropped: every node whose TypeDefinition is
69
+ `DataTypeDictionaryType` or `DataTypeDescriptionType`, and everything they own. The structure
70
+ DataTypes are kept, with their `DataTypeDefinition`. On the DI nodeset that is 14 nodes of 447.
71
+
72
+ [`VOCABULARY.md`](./VOCABULARY.md) is the full account: prefixes, IRI minting, the ontology node,
73
+ type composition, the reference rules, the three kinds of `@included` entry.
74
+
75
+ ## Which entry point
76
+
77
+ That document can be produced two ways, and each has an entry point of its own so that neither
78
+ drags in what only the other needs.
79
+
80
+ | | [`/address-space`](#node-opcua-uanodeset-rdfaddress-space) | [`/session`](#node-opcua-uanodeset-rdfsession) |
81
+ |---|---|---|
82
+ | you have | the NodeSet2 document | an `OPCUAClient` or a `PseudoSession` |
83
+ | covers | every node of the namespace | every node reachable by browsing |
84
+ | `symbolicName` | yes | no |
85
+ | `owl:imports` | yes | no |
86
+
87
+ Prefer `/address-space` when you have the document. Reach for `/session` when you do not, which
88
+ for a vendor's own companion specification is often the only option there is.
89
+
90
+ The root module re-exports both, for callers that want both.
91
+
92
+ ---
93
+
94
+ # `node-opcua-uanodeset-rdf/address-space`
95
+
96
+ The complete source. An address space retains the NodeSet2 metadata that OPC UA has no attribute
97
+ for, so an export taken this way is the reference the other is judged against.
98
+
99
+ ```ts
100
+ import { writeFile } from "node:fs/promises";
101
+ import { AddressSpace } from "node-opcua-address-space";
102
+ import { generateAddressSpace } from "node-opcua-address-space/nodeJS";
103
+ import { nodesets } from "node-opcua-nodesets";
104
+ import { addressSpaceToJsonLdText } from "node-opcua-uanodeset-rdf/address-space";
105
+
106
+ const addressSpace = AddressSpace.create();
107
+ await generateAddressSpace(addressSpace, [nodesets.standard, nodesets.di]);
108
+
109
+ // with no modelUri, the last namespace loaded is the one exported
110
+ await writeFile("Opc.Ua.Di.jsonld", addressSpaceToJsonLdText(addressSpace), "utf8");
111
+
112
+ addressSpace.dispose();
113
+ ```
114
+
115
+ A whole address space is needed rather than the one namespace, and that is not an implementation
116
+ detail: a type's IRI is its BrowseName, including for types belonging to the models this one
117
+ merely imports, so the exporter has to resolve NodeIds it never read.
118
+
119
+ Two fields of the ontology node are written in the source document's `Models` element and are not
120
+ retained by the loader. Supply them if you want them in the output:
121
+
122
+ ```ts
123
+ import { addressSpaceToJsonLd } from "node-opcua-uanodeset-rdf/address-space";
124
+
125
+ const doc = addressSpaceToJsonLd(addressSpace, {
126
+ modelUri: "http://opcfoundation.org/UA/DI/",
127
+ model: { modelVersion: "1.5.0", xmlSchemaUri: "http://opcfoundation.org/UA/DI/Types.xsd" }
128
+ });
129
+ ```
130
+
131
+ | export | what it is |
132
+ |---|---|
133
+ | `addressSpaceToJsonLdText(addressSpace, options?)` | one namespace as a JSON-LD document, serialised |
134
+ | `addressSpaceToJsonLd(addressSpace, options?)` | the same as a plain object, to hand to a JSON-LD library |
135
+ | `addressSpaceToRdfModel(addressSpace, modelUri?)` | the collected model, to inspect or amend before mapping it |
136
+
137
+ ---
138
+
139
+ # `node-opcua-uanodeset-rdf/session`
140
+
141
+ Nothing reachable from this entry point mentions `node-opcua-address-space`, at the type level or
142
+ the runtime one, so a client application uses it without loading the address-space machinery it
143
+ has no other use for.
144
+
145
+ ## With an `OPCUAClient`
146
+
147
+ This is the case a nodeset file cannot serve: a device implementing a companion specification you
148
+ do not have, exported straight into a triple store.
149
+
150
+ ```ts
151
+ import { writeFile } from "node:fs/promises";
152
+ import { OPCUAClient } from "node-opcua-client";
153
+ import { sessionToJsonLdText } from "node-opcua-uanodeset-rdf/session";
154
+
155
+ await OPCUAClient.create({ endpointMustExist: false }).withSessionAsync(
156
+ "opc.tcp://localhost:26543",
157
+ async (session) => {
158
+ const jsonld = await sessionToJsonLdText(session, {
159
+ modelUri: "http://acme.com/UA/Widget/"
160
+ });
161
+ await writeFile("Widget.jsonld", jsonld, "utf8");
162
+ }
163
+ );
164
+ ```
165
+
166
+ ## With a `PseudoSession`
167
+
168
+ `PseudoSession` puts the session interface in front of an address space you already hold. No
169
+ server, no socket, no endpoint.
170
+
171
+ ```ts
172
+ import { AddressSpace, PseudoSession } from "node-opcua-address-space";
173
+ import { generateAddressSpace } from "node-opcua-address-space/nodeJS";
174
+ import { nodesets } from "node-opcua-nodesets";
175
+ import { sessionToJsonLdText } from "node-opcua-uanodeset-rdf/session";
176
+
177
+ const addressSpace = AddressSpace.create();
178
+ await generateAddressSpace(addressSpace, [nodesets.standard, nodesets.di]);
179
+
180
+ const jsonld = await sessionToJsonLdText(new PseudoSession(addressSpace), {
181
+ modelUri: "http://opcfoundation.org/UA/DI/"
182
+ });
183
+
184
+ addressSpace.dispose();
185
+ ```
186
+
187
+ It is also how this entry point is tested. Both paths then see the same model, so every
188
+ difference between the two documents is something OPC UA cannot say about a node rather than
189
+ something the collector got wrong.
190
+
191
+ ## What a session has to provide
192
+
193
+ `RdfSession` is not a session type. It is the three services collecting a model actually calls:
194
+
195
+ ```ts
196
+ type RdfSession = IBasicSessionBrowseAsyncMultiple &
197
+ IBasicSessionBrowseNextAsyncMultiple &
198
+ IBasicSessionReadAsyncMultiple;
199
+ ```
200
+
201
+ Browse, follow the continuation point a browse hands back, read attributes. `create`, `close`,
202
+ subscriptions and the rest of a session's surface are deliberately outside it, so a caller may
203
+ pass something much smaller than a session.
204
+
205
+ Browse is chunked by the server's own `MaxNodesPerBrowse` and `MaxBrowseContinuationPoints`,
206
+ which `browseAll` reads for itself. Attribute reads use a fixed modest batch: honouring a
207
+ server's `MaxNodesPerRead` belongs in a client that specialises in it, not in an exporter.
208
+
209
+ ## What a session cannot tell you
210
+
211
+ Three differences from the address-space export, all structural rather than unfinished work, each
212
+ measured by exporting one DI address space both ways.
213
+
214
+ - **No service lists the nodes of a namespace.** Browse walks references; nothing enumerates. So
215
+ the model is whatever a crawl from Root reaches, and a node nothing points at is invisible. On
216
+ DI that is exactly nine: the well-known function-group Objects `Configuration`, `Tuning`,
217
+ `Maintenance`, `Diagnostics`, `Statistics`, `Status`, `Operational`, `OperationCounters` and
218
+ `Identification`, which carry no inverse reference of any type.
219
+ - **`SymbolicName` is not an attribute.** It is NodeSet2 metadata, so `uardf:symbolicName` is
220
+ absent from every entry.
221
+ - **`NamespaceMetadataType` states no required models.** It carries `NamespaceUri`,
222
+ `NamespaceVersion` and `NamespacePublicationDate` and no list of imports, so the ontology node
223
+ has `version` and `publicationDate` but no `requiredModels` and no `owl:imports`.
224
+
225
+ Everything else agrees, node for node and predicate for predicate: the same `@id`, `@type`,
226
+ `dataType`, `value` and children, the same `@context` terms, the same `@included` entries.
227
+
228
+ | export | what it is |
229
+ |---|---|
230
+ | `sessionToJsonLdText(session, options?)` | one namespace as a JSON-LD document, serialised |
231
+ | `sessionToJsonLd(session, options?)` | the same as a plain object |
232
+ | `sessionToRdfModel(session, modelUri?)` | the collected model, to inspect or amend before mapping it |
233
+ | `RdfSession` | the intersection above |
234
+
235
+ ---
236
+
237
+ ## Shared API
238
+
239
+ Both entry points also export:
240
+
241
+ | export | what it is |
242
+ |---|---|
243
+ | `JsonLdOptions` | `{ modelUri?, model?: { xmlSchemaUri?, modelVersion? } }` |
244
+ | `modelToJsonLd(model, options?)` | the mapping itself, for a model collected some third way |
245
+ | `RdfModel`, `RdfNode`, `RdfReference`, `RdfTarget` | the collected model's types |
246
+ | `UARDF` | `http://opcfoundation.org/rdf/uacore#`, the vocabulary namespace |
247
+ | `OPCUA_NAMESPACE` | `http://opcfoundation.org/UA/` |
248
+ | `prefixOfNamespace(uri)` | the prefix a namespace URI gets: its last non-empty path segment, lowercased |
249
+
250
+ Collecting a model and mapping it are separate on purpose. `modelToJsonLd` is synchronous and
251
+ reads nothing but an `RdfModel`, which is why the session collector could be added without
252
+ touching a line of the vocabulary, and why a third source would need no more than a function
253
+ returning an `RdfModel`.
254
+
255
+ ## Status
256
+
257
+ **This is an export, not a serialisation format.** There is no reader, and it never registers
258
+ with the address-space loader. To read or write a JSON nodeset you want
259
+ [`node-opcua-uanodeset-json`](../node-opcua-uanodeset-json), which implements the normative
260
+ Annex I formats. The two answer different questions: Annex I is how a model is exchanged, this is
261
+ how a model is queried.
262
+
263
+ **The vocabulary is not in the specification.** OPC 10000-6 Annex I runs I.1 to I.24 and has no
264
+ RDF section. This vocabulary belongs to the OPC Foundation's `UA-NodeSetTool`, whose own
265
+ `rdf_prototype.md` no longer describes what it emits, so its output *is* the specification and
266
+ `VOCABULARY.md` is a reverse-engineered account of it.
267
+
268
+ **Parity** with that tool, measured on `Opc.Ua.Di.NodeSet2`:
269
+
270
+ | | reference tool | this package |
271
+ |---|---|---|
272
+ | `@graph` entries | 434 | **434** |
273
+ | `@included` entries | 172 | 173 |
274
+ | `@context` terms | 239 | 220 |
275
+ | entries byte-identical | n/a | 66 of 434 |
276
+
277
+ Graph membership matches exactly, so a query over either document sees the same nodes. It is not
278
+ yet byte-identical, and with no reader there is no round trip, no fixpoint and no digest
279
+ equivalence to lean on: a diff against the reference implementation is the only oracle. The gap
280
+ is one undocumented rule, whether a child is written under a BrowseName-derived predicate or
281
+ under the generic reference type. Three hypotheses were tested and all three failed, so the
282
+ question is recorded in `VOCABULARY.md` rather than answered with a rule that merely fits one
283
+ nodeset.
284
+
285
+ ## On the name
286
+
287
+ `node-opcua-nodeset-*` is a reserved prefix. The nodeset code generator sweeps every folder
288
+ matching it and rewrites `packages/tsconfig.json` from what it finds, so a hand-written package
289
+ sitting there breaks `generate:nodesets`. Hence `uanodeset`, matching
290
+ `node-opcua-uanodeset-json`.
291
+
292
+ ## License
293
+
294
+ MIT
package/VOCABULARY.md ADDED
@@ -0,0 +1,186 @@
1
+ # The OPC UA JSON-LD vocabulary
2
+
3
+ This is a **reverse-engineered** description of the JSON-LD an OPC UA information model is
4
+ exported as by `OPCF-Members/UA-NodeSetTool`. It is written down here because no accurate
5
+ description existed: the tool's own `rdf_prototype.md` describes an earlier shape, and the
6
+ differences are not small.
7
+
8
+ Nothing here is normative. **OPC 10000-6 Annex I has no RDF section** — the annex runs I.1 to
9
+ I.24 and stops at `DataTypeField`. This vocabulary is the reference implementation's own.
10
+
11
+ Everything below was established by generating output for real nodesets and reading it. Where a
12
+ rule could not be established, it says so rather than guessing.
13
+
14
+ ---
15
+
16
+ ## Document shape
17
+
18
+ ```jsonc
19
+ {
20
+ "@context": { /* prefixes, property terms, and per-document predicates */ },
21
+ "@graph": [ /* the ontology node, then one entry per node */ ],
22
+ "@included": [ /* OWL property declarations, and foreign attachments */ ]
23
+ }
24
+ ```
25
+
26
+ There is no root `@id` and no root `@type`. (`rdf_prototype.md` describes both, plus `modelUri`,
27
+ `version` and `requiredModels` at the root, and nodes flagged `isNormative` / `isExternal`. None
28
+ of that is emitted.)
29
+
30
+ ## Prefixes
31
+
32
+ | prefix | expands to |
33
+ |---|---|
34
+ | `opcua` | `http://opcfoundation.org/UA/` |
35
+ | `uardf` | `http://opcfoundation.org/rdf/uacore#` |
36
+ | `owl` | `http://www.w3.org/2002/07/owl#` |
37
+ | `rdfs` | `http://www.w3.org/2000/01/rdf-schema#` |
38
+ | `xsd` | `http://www.w3.org/2001/XMLSchema#` |
39
+ | *model* | the exported model's namespace URI |
40
+
41
+ The model's prefix is its namespace URI's **last non-empty path segment, lowercased** —
42
+ `http://opcfoundation.org/UA/DI/` becomes `di`.
43
+
44
+ Note the vocabulary prefix is `uardf`, **not** `opcua` as `rdf_prototype.md` states. Only the
45
+ reference types and BrowseName-derived predicates live under `opcua:`.
46
+
47
+ ## Node identity
48
+
49
+ | node kind | IRI |
50
+ |---|---|
51
+ | ObjectType, VariableType, ReferenceType, DataType | `<prefix>:<BrowseName>` |
52
+ | everything else | `<prefix>:<base64url(canonical NodeId)>` |
53
+
54
+ The canonical NodeId is the Annex I form (`i=85`, or `nsu=<uri>;i=85`), base64url encoded
55
+ **without padding**. So `nsu=http://opcfoundation.org/UA/DI/;i=6450` becomes
56
+ `di:bnN1PWh0dHA6Ly9vcGNmb3VuZGF0aW9uLm9yZy9VQS9ESS87aT02NDUw`.
57
+
58
+ ## The ontology node
59
+
60
+ The first entry of `@graph` describes the model itself:
61
+
62
+ ```jsonc
63
+ {
64
+ "@id": "http://opcfoundation.org/UA/DI/",
65
+ "@type": ["uardf:UANodeSet", "owl:Ontology"],
66
+ "modelUri": "...", "xmlSchemaUri": "...",
67
+ "version": "1.05.0", "modelVersion": "1.5.0",
68
+ "owl:versionInfo": "1.5.0",
69
+ "owl:versionIRI": { "@id": "<modelUri><modelVersion>" },
70
+ "publicationDate": "2025-11-15T00:00:00Z",
71
+ "requiredModels": [ { "modelUri": "...", "version": "...", "publicationDate": "..." } ],
72
+ "owl:imports": [ { "@id": "http://opcfoundation.org/UA/" } ]
73
+ }
74
+ ```
75
+
76
+ `xmlSchemaUri` and `modelVersion` are written in the source document's `Models` element and are
77
+ **not retained by node-opcua's address space**, so this package takes them through
78
+ `JsonLdOptions.model` rather than inventing them.
79
+
80
+ ## Node entries
81
+
82
+ Every node carries `@id`, `@type`, `nodeId`, `namespaceUri`, `browseName` and `name`.
83
+ `browseName` is the Annex I QualifiedName form (bare in namespace 0, `nsu=<uri>;<name>`
84
+ otherwise); `name` is the bare BrowseName.
85
+
86
+ `@type` is `uardf:UAObject`, `uardf:UAVariable`, `uardf:UAMethod`, `uardf:UAObjectType`,
87
+ `uardf:UAVariableType`, `uardf:UAReferenceType`, `uardf:UADataType` or `uardf:UAView`, plus:
88
+
89
+ - `owl:Class` for the four type NodeClasses;
90
+ - the **TypeDefinition's IRI** for instances, so a Property is `["uardf:UAVariable",
91
+ "opcua:PropertyType"]`.
92
+
93
+ Other fields: `subClassOf` (from the inverse `HasSubtype`), `parent`, `symbolicName`,
94
+ `description` as a language map (`{"@none": "..."}` when no locale), `dataType`, `valueRank`
95
+ (omitted when scalar), `arrayDimensions`, `definition`, and `value` as a raw Part 6 1.05 JSON
96
+ Variant carried as a `@json` literal.
97
+
98
+ ## References
99
+
100
+ `HasTypeDefinition`, `HasModellingRule` and `HasSubtype` are never written as references:
101
+ the first two become `@type` and `modellingRule`, the third becomes `subClassOf`.
102
+
103
+ Everything else is written one of two ways:
104
+
105
+ - **as a predicate named after the child's BrowseName** — `di:ParameterSet`, `opcua:EnumStrings`;
106
+ - **as a predicate named after the reference type** — `opcua:HasComponent`, whose value is an
107
+ array when several children share it.
108
+
109
+ **The rule choosing between the two is not established.** Observed on the DI nodeset:
110
+ `IdleToPreparing` and `ParameterSet` get BrowseName predicates; `Idle`, `Error`, `Installing`
111
+ and `Abort` do not, and appear under `opcua:HasComponent` instead. It is not name uniqueness
112
+ (`PercentComplete` occurs twice and *does* get one; `Error` occurs once and does not), and it is
113
+ not the NodeClass or the reference type (States and Transitions are both Objects reached by
114
+ `HasComponent`, and they differ). **This is the open question to put to the maintainer.**
115
+
116
+ A BrowseName that names a placeholder (`<ParameterIdentifier>`) never becomes a predicate.
117
+
118
+ ## `@included`
119
+
120
+ Three kinds of entry:
121
+
122
+ 1. **ReferenceType declarations**, one per reference type the model defines:
123
+ `{"@id", "@type": "owl:ObjectProperty", "label", "symmetric", "inverseOf", "inverseName",
124
+ "subPropertyOf"}`. A symmetric type is its own inverse.
125
+ 2. **Predicate declarations**, one per BrowseName-derived predicate, minimal:
126
+ `{"@id", "@type": "owl:ObjectProperty", "subPropertyOf": "<the reference type reaching it>"}`.
127
+ 3. **Foreign attachments**: a node in *another* model that this model hangs children on, written
128
+ as `{"@id": "<that node>", "<predicate>": "<child>"}` with no `@type`. It cannot go in
129
+ `@graph`, because the document does not define that node.
130
+
131
+ ## What is dropped
132
+
133
+ The legacy OPC Binary machinery: every node whose TypeDefinition is `DataTypeDictionaryType`
134
+ (`i=72`) or `DataTypeDescriptionType` (`i=69`), **and everything those nodes own**. The structure
135
+ DataTypes themselves are kept, with their `DataTypeDefinition`; only the encoding dictionaries and
136
+ description blobs go. On the DI nodeset that is 14 nodes of 448.
137
+
138
+ ---
139
+
140
+ ## Parity status of this implementation
141
+
142
+ Measured against the reference implementation for `Opc.Ua.Di.NodeSet2`:
143
+
144
+ | | reference | this package |
145
+ |---|---|---|
146
+ | `@graph` entries | 434 | **434** |
147
+ | `@included` entries | 172 | 173 |
148
+ | `@context` terms | 239 | 220 |
149
+ | entries byte-identical | — | 66 of 434 |
150
+
151
+ `@graph` membership matches exactly. The remaining differences are concentrated in which
152
+ predicate a child is written under (the open question above) and in the reference-type aliases
153
+ that `@context` needs so the `subPropertyOf` values in `@included` resolve.
154
+
155
+ **This exporter is not yet a drop-in replacement**, and the numbers above are the honest state of
156
+ it rather than an aspiration.
157
+
158
+ ---
159
+
160
+ ## What a session cannot tell you
161
+
162
+ The same document can be produced from a live session
163
+ (`node-opcua-uanodeset-rdf/session`) rather than a loaded address space. That is the only way to
164
+ export a server whose nodeset file you do not have, and it is not the equal of the address-space
165
+ path. Three differences are structural, not defects waiting to be fixed, and each was measured on
166
+ DI by exporting the same address space both ways.
167
+
168
+ **1. There is no service that lists the nodes of a namespace.** Browse walks references; nothing
169
+ enumerates. So a session-sourced model is whatever a crawl from Root reaches, and a node nothing
170
+ points at is invisible. On DI that is nine nodes: the well-known function-group Objects
171
+ `Configuration`, `Tuning`, `Maintenance`, `Diagnostics`, `Statistics`, `Status`, `Operational`,
172
+ `OperationCounters` and `Identification`. They carry no inverse reference of any type, so no crawl
173
+ from any root reaches them. Every other node of the namespace is reached, and each is written with
174
+ the same `@id`, `@type`, `dataType`, `value` and predicates as the address-space export gives it.
175
+
176
+ **2. `SymbolicName` is not an attribute.** It is NodeSet2 metadata, so `uardf:symbolicName` is
177
+ absent from every entry of a session-sourced document.
178
+
179
+ **3. `NamespaceMetadataType` states no required models.** It carries `NamespaceUri`,
180
+ `NamespaceVersion` and `NamespacePublicationDate`, and no list of imports. So the ontology node of
181
+ a session-sourced document has `version` and `publicationDate` but no `requiredModels` and no
182
+ `owl:imports`, which is what a reasoner uses to pull the imported models in. A caller that knows
183
+ the imports can still supply `xmlSchemaUri` and `modelVersion` through `JsonLdOptions.model`.
184
+
185
+ Everything else agrees: the `@context` terms, the `@included` entries, the drop rule for the
186
+ legacy OPC Binary machinery, and every predicate on every node reached.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The address-space entry point: `node-opcua-uanodeset-rdf/address-space`.
3
+ *
4
+ * This is the complete source and the one to prefer where the model has been loaded from its
5
+ * NodeSet2 document: an address space retains the metadata OPC UA has no attribute for, so an
6
+ * export taken this way is the reference a session-sourced one is judged against.
7
+ */
8
+ import type { IAddressSpace } from "node-opcua-address-space";
9
+ import { type JsonLdOptions } from "./to_jsonld.js";
10
+ export { addressSpaceToRdfModel } from "./from_address_space.js";
11
+ export type { RdfModel, RdfModelReference, RdfNode, RdfReference, RdfTarget } from "./model.js";
12
+ export { type JsonLdOptions, modelToJsonLd, OPCUA_NAMESPACE, prefixOfNamespace, UARDF } from "./to_jsonld.js";
13
+ /** a namespace of an address space as a JSON-LD document */
14
+ export declare function addressSpaceToJsonLd(addressSpace: IAddressSpace, options?: JsonLdOptions): Record<string, unknown>;
15
+ /** a namespace of an address space as a JSON-LD document, serialised */
16
+ export declare function addressSpaceToJsonLdText(addressSpace: IAddressSpace, options?: JsonLdOptions): string;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * The address-space entry point: `node-opcua-uanodeset-rdf/address-space`.
4
+ *
5
+ * This is the complete source and the one to prefer where the model has been loaded from its
6
+ * NodeSet2 document: an address space retains the metadata OPC UA has no attribute for, so an
7
+ * export taken this way is the reference a session-sourced one is judged against.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.UARDF = exports.prefixOfNamespace = exports.OPCUA_NAMESPACE = exports.modelToJsonLd = exports.addressSpaceToRdfModel = void 0;
11
+ exports.addressSpaceToJsonLd = addressSpaceToJsonLd;
12
+ exports.addressSpaceToJsonLdText = addressSpaceToJsonLdText;
13
+ const from_address_space_js_1 = require("./from_address_space.js");
14
+ const to_jsonld_js_1 = require("./to_jsonld.js");
15
+ var from_address_space_js_2 = require("./from_address_space.js");
16
+ Object.defineProperty(exports, "addressSpaceToRdfModel", { enumerable: true, get: function () { return from_address_space_js_2.addressSpaceToRdfModel; } });
17
+ var to_jsonld_js_2 = require("./to_jsonld.js");
18
+ Object.defineProperty(exports, "modelToJsonLd", { enumerable: true, get: function () { return to_jsonld_js_2.modelToJsonLd; } });
19
+ Object.defineProperty(exports, "OPCUA_NAMESPACE", { enumerable: true, get: function () { return to_jsonld_js_2.OPCUA_NAMESPACE; } });
20
+ Object.defineProperty(exports, "prefixOfNamespace", { enumerable: true, get: function () { return to_jsonld_js_2.prefixOfNamespace; } });
21
+ Object.defineProperty(exports, "UARDF", { enumerable: true, get: function () { return to_jsonld_js_2.UARDF; } });
22
+ /** a namespace of an address space as a JSON-LD document */
23
+ function addressSpaceToJsonLd(addressSpace, options = {}) {
24
+ return (0, to_jsonld_js_1.modelToJsonLd)((0, from_address_space_js_1.addressSpaceToRdfModel)(addressSpace, options.modelUri), options);
25
+ }
26
+ /** a namespace of an address space as a JSON-LD document, serialised */
27
+ function addressSpaceToJsonLdText(addressSpace, options = {}) {
28
+ return `${JSON.stringify(addressSpaceToJsonLd(addressSpace, options), null, 2)}\n`;
29
+ }
30
+ //# sourceMappingURL=address_space.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address_space.js","sourceRoot":"","sources":["../source/address_space.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAWH,oDAEC;AAGD,4DAEC;AAfD,mEAAiE;AACjE,iDAAmE;AAEnE,iEAAiE;AAAxD,+HAAA,sBAAsB,OAAA;AAE/B,+CAA8G;AAAjF,6GAAA,aAAa,OAAA;AAAE,+GAAA,eAAe,OAAA;AAAE,iHAAA,iBAAiB,OAAA;AAAE,qGAAA,KAAK,OAAA;AAErF,4DAA4D;AAC5D,SAAgB,oBAAoB,CAAC,YAA2B,EAAE,UAAyB,EAAE;IACzF,OAAO,IAAA,4BAAa,EAAC,IAAA,8CAAsB,EAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AAC1F,CAAC;AAED,wEAAwE;AACxE,SAAgB,wBAAwB,CAAC,YAA2B,EAAE,UAAyB,EAAE;IAC7F,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AACvF,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Collecting an `RdfModel` from a loaded address space.
3
+ *
4
+ * This is the complete source: an address space retains the NodeSet2 metadata that OPC UA has no
5
+ * attribute for, so an export taken this way is the reference against which a session-sourced one
6
+ * is judged.
7
+ *
8
+ * Foreign nodes are converted lazily. The mapper names supertypes, reference types and type
9
+ * definitions belonging to models this one only imports, and there is no point walking all of
10
+ * `Opc.Ua` to find the handful it actually asks for.
11
+ */
12
+ import type { IAddressSpace } from "node-opcua-address-space";
13
+ import type { RdfModel } from "./model.js";
14
+ /** the model a namespace of an address space describes */
15
+ export declare function addressSpaceToRdfModel(addressSpace: IAddressSpace, modelUri?: string): RdfModel;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ /**
3
+ * Collecting an `RdfModel` from a loaded address space.
4
+ *
5
+ * This is the complete source: an address space retains the NodeSet2 metadata that OPC UA has no
6
+ * attribute for, so an export taken this way is the reference against which a session-sourced one
7
+ * is judged.
8
+ *
9
+ * Foreign nodes are converted lazily. The mapper names supertypes, reference types and type
10
+ * definitions belonging to models this one only imports, and there is no point walking all of
11
+ * `Opc.Ua` to find the handful it actually asks for.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.addressSpaceToRdfModel = addressSpaceToRdfModel;
15
+ const node_opcua_data_model_1 = require("node-opcua-data-model");
16
+ /**
17
+ * one node.
18
+ *
19
+ * `full` is false for a node the exporter only has to name. Skipping the value read there is not
20
+ * an optimisation for its own sake: reading a value can be expensive, or fail, on a node this
21
+ * document never describes.
22
+ */
23
+ function toRdfNode(node, full) {
24
+ const out = {
25
+ nodeId: node.nodeId,
26
+ nodeClass: node.nodeClass,
27
+ browseName: node.browseName,
28
+ references: node.allReferences().map((reference) => ({
29
+ referenceTypeId: reference.referenceType,
30
+ isForward: reference.isForward,
31
+ targetId: reference.nodeId
32
+ }))
33
+ };
34
+ const typeDefinition = node.typeDefinitionObj?.nodeId;
35
+ if (typeDefinition)
36
+ out.typeDefinition = typeDefinition;
37
+ const supertype = node.subtypeOfObj?.nodeId;
38
+ if (supertype)
39
+ out.subtypeOf = supertype;
40
+ if (node.description?.text)
41
+ out.description = node.description;
42
+ if (node.nodeClass === node_opcua_data_model_1.NodeClass.ReferenceType) {
43
+ const referenceType = node;
44
+ out.symmetric = !!referenceType.symmetric;
45
+ if (referenceType.inverseName)
46
+ out.inverseName = referenceType.inverseName;
47
+ }
48
+ if (!full)
49
+ return out;
50
+ const symbolicName = node.symbolicName;
51
+ if (symbolicName)
52
+ out.symbolicName = symbolicName;
53
+ if (node.nodeClass === node_opcua_data_model_1.NodeClass.Variable || node.nodeClass === node_opcua_data_model_1.NodeClass.VariableType) {
54
+ const variable = node;
55
+ if (variable.dataType)
56
+ out.dataType = variable.dataType;
57
+ if (variable.valueRank !== undefined)
58
+ out.valueRank = variable.valueRank;
59
+ if (variable.arrayDimensions)
60
+ out.arrayDimensions = variable.arrayDimensions;
61
+ try {
62
+ const dataValue = variable.readValue();
63
+ if (dataValue?.value)
64
+ out.value = dataValue.value;
65
+ }
66
+ catch {
67
+ // a value that cannot be read contributes none, which is not an error for an export
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+ /** the model a namespace of an address space describes */
73
+ function addressSpaceToRdfModel(addressSpace, modelUri) {
74
+ const namespaces = addressSpace.getNamespaceArray();
75
+ const target = modelUri ? namespaces.find((n) => n.namespaceUri === modelUri) : namespaces[namespaces.length - 1];
76
+ if (!target) {
77
+ throw new Error(`the address space holds no namespace ${modelUri}`);
78
+ }
79
+ const cache = new Map();
80
+ const nodes = [];
81
+ for (const node of namespaces[target.index].nodeIterator()) {
82
+ const converted = toRdfNode(node, true);
83
+ cache.set(node.nodeId.toString(), converted);
84
+ nodes.push(converted);
85
+ }
86
+ return {
87
+ namespaceUris: namespaces.map((n) => n.namespaceUri),
88
+ target: {
89
+ index: target.index,
90
+ namespaceUri: target.namespaceUri,
91
+ version: target.version,
92
+ publicationDate: target.publicationDate,
93
+ requiredModels: target.getRequiredModels?.()
94
+ },
95
+ nodes,
96
+ resolve(nodeId) {
97
+ const key = nodeId.toString();
98
+ const known = cache.get(key);
99
+ if (known)
100
+ return known;
101
+ const found = addressSpace.findNode(nodeId);
102
+ if (!found)
103
+ return undefined;
104
+ const converted = toRdfNode(found, false);
105
+ cache.set(key, converted);
106
+ return converted;
107
+ }
108
+ };
109
+ }
110
+ //# sourceMappingURL=from_address_space.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"from_address_space.js","sourceRoot":"","sources":["../source/from_address_space.ts"],"names":[],"mappings":"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"}