@alizarin/napi 0.2.1-alpha.83
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/Cargo.toml +21 -0
- package/__test__/index.mjs +533 -0
- package/alizarin-napi.linux-x64-gnu.node +0 -0
- package/bin/csv-to-prebuild.mjs +143 -0
- package/build.rs +5 -0
- package/index.d.ts +329 -0
- package/index.js +334 -0
- package/package.json +32 -0
- package/src/instance_wrapper_napi.rs +1699 -0
- package/src/lib.rs +611 -0
|
@@ -0,0 +1,1699 @@
|
|
|
1
|
+
/// NAPI bindings for ResourceInstanceWrapperCore
|
|
2
|
+
///
|
|
3
|
+
/// Provides the same public interface as WASMResourceInstanceWrapper so that
|
|
4
|
+
/// the TypeScript ViewModel layer (ResourceInstanceWrapper / PseudoValue / PseudoList)
|
|
5
|
+
/// can work with either backend.
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
use napi::bindgen_prelude::*;
|
|
10
|
+
use napi_derive::napi;
|
|
11
|
+
|
|
12
|
+
use std::collections::HashSet;
|
|
13
|
+
|
|
14
|
+
use alizarin_core::extension_type_registry::ExtensionTypeRegistry;
|
|
15
|
+
use alizarin_core::graph::{
|
|
16
|
+
StaticEdge, StaticGraph, StaticNode, StaticNodegroup, StaticResource, StaticTile,
|
|
17
|
+
};
|
|
18
|
+
use alizarin_core::instance_wrapper_core::ModelAccess;
|
|
19
|
+
use alizarin_core::instance_wrapper_core::{
|
|
20
|
+
LoadState, ResourceInstanceWrapperCore, SemanticChildError, SemanticChildResult,
|
|
21
|
+
};
|
|
22
|
+
use alizarin_core::node_config::NodeConfigManager;
|
|
23
|
+
use alizarin_core::permissions::PermissionRule;
|
|
24
|
+
use alizarin_core::pseudo_value_core::{PseudoListCore, PseudoValueCore, VisitorContext};
|
|
25
|
+
use alizarin_core::rdm_cache::RdmCache;
|
|
26
|
+
use alizarin_core::type_serialization::{SerializationContext, SerializationOptions};
|
|
27
|
+
use alizarin_core::GraphModelAccess;
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Helpers
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
fn ext_registry() -> &'static ExtensionTypeRegistry {
|
|
34
|
+
crate::extension_registry()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn sc_err(e: SemanticChildError) -> napi::Error {
|
|
38
|
+
napi::Error::from_reason(e.to_string())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// NapiRdmCache
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
#[napi]
|
|
46
|
+
pub struct NapiRdmCache {
|
|
47
|
+
inner: RdmCache,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[napi]
|
|
51
|
+
impl NapiRdmCache {
|
|
52
|
+
#[napi(constructor)]
|
|
53
|
+
pub fn new() -> Self {
|
|
54
|
+
NapiRdmCache {
|
|
55
|
+
inner: RdmCache::new(),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Load collections from a SKOS JSON string.
|
|
60
|
+
#[napi]
|
|
61
|
+
pub fn load_from_skos_json(&mut self, json_str: String) -> Result<()> {
|
|
62
|
+
let collections: Vec<alizarin_core::skos::SkosCollection> = serde_json::from_str(&json_str)
|
|
63
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid SKOS JSON: {e}")))?;
|
|
64
|
+
for collection in collections {
|
|
65
|
+
let rdm_col = alizarin_core::skos_to_rdm_collection(&collection);
|
|
66
|
+
self.inner.add_collection(rdm_col);
|
|
67
|
+
}
|
|
68
|
+
Ok(())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Load a single collection from JSON.
|
|
72
|
+
#[napi]
|
|
73
|
+
pub fn load_collection_json(&mut self, json_str: String) -> Result<()> {
|
|
74
|
+
let collection: alizarin_core::skos::SkosCollection = serde_json::from_str(&json_str)
|
|
75
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid collection JSON: {e}")))?;
|
|
76
|
+
let rdm_col = alizarin_core::skos_to_rdm_collection(&collection);
|
|
77
|
+
self.inner.add_collection(rdm_col);
|
|
78
|
+
Ok(())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[napi(getter)]
|
|
82
|
+
pub fn collection_count(&self) -> u32 {
|
|
83
|
+
self.inner.len() as u32
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// =============================================================================
|
|
88
|
+
// NapiNodeConfigManager
|
|
89
|
+
// =============================================================================
|
|
90
|
+
|
|
91
|
+
#[napi]
|
|
92
|
+
pub struct NapiNodeConfigManager {
|
|
93
|
+
inner: NodeConfigManager,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[napi]
|
|
97
|
+
impl NapiNodeConfigManager {
|
|
98
|
+
#[napi(constructor)]
|
|
99
|
+
pub fn new() -> Self {
|
|
100
|
+
NapiNodeConfigManager {
|
|
101
|
+
inner: NodeConfigManager::new(),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Build node configs from a graph JSON string.
|
|
106
|
+
#[napi]
|
|
107
|
+
pub fn build_from_graph_json(&mut self, graph_json: String) -> Result<()> {
|
|
108
|
+
self.inner
|
|
109
|
+
.from_graph_json(&graph_json)
|
|
110
|
+
.map_err(napi::Error::from_reason)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Build node configs from a NapiStaticGraph.
|
|
114
|
+
#[napi]
|
|
115
|
+
pub fn build_from_graph(&mut self, graph: &crate::NapiStaticGraph) -> Result<()> {
|
|
116
|
+
self.inner.build_from_graph(graph.inner_ref());
|
|
117
|
+
Ok(())
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// NapiPseudoValue
|
|
123
|
+
// =============================================================================
|
|
124
|
+
|
|
125
|
+
#[napi]
|
|
126
|
+
pub struct NapiPseudoValue {
|
|
127
|
+
inner: PseudoValueCore,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[napi]
|
|
131
|
+
impl NapiPseudoValue {
|
|
132
|
+
// -- Node metadata getters --
|
|
133
|
+
|
|
134
|
+
#[napi(getter)]
|
|
135
|
+
pub fn node(&self) -> serde_json::Value {
|
|
136
|
+
serde_json::to_value(&*self.inner.node).unwrap_or(serde_json::Value::Null)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[napi(getter)]
|
|
140
|
+
pub fn node_id(&self) -> Option<String> {
|
|
141
|
+
Some(self.inner.node.nodeid.clone())
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[napi(getter)]
|
|
145
|
+
pub fn node_alias(&self) -> Option<String> {
|
|
146
|
+
self.inner.node.alias.clone()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[napi(getter)]
|
|
150
|
+
pub fn datatype(&self) -> String {
|
|
151
|
+
self.inner.node.datatype.clone()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#[napi(getter)]
|
|
155
|
+
pub fn nodegroup_id(&self) -> Option<String> {
|
|
156
|
+
self.inner.node.nodegroup_id.clone()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[napi(getter)]
|
|
160
|
+
pub fn is_collector(&self) -> bool {
|
|
161
|
+
self.inner.is_collector
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#[napi(getter)]
|
|
165
|
+
pub fn independent(&self) -> bool {
|
|
166
|
+
self.inner.independent
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[napi(getter)]
|
|
170
|
+
pub fn exportable(&self) -> bool {
|
|
171
|
+
self.inner.node.exportable
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[napi(getter)]
|
|
175
|
+
pub fn isrequired(&self) -> bool {
|
|
176
|
+
self.inner.node.isrequired
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[napi(getter)]
|
|
180
|
+
pub fn issearchable(&self) -> bool {
|
|
181
|
+
self.inner.node.issearchable
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[napi(getter)]
|
|
185
|
+
pub fn ontologyclass(&self) -> serde_json::Value {
|
|
186
|
+
serde_json::to_value(&self.inner.node.ontologyclass).unwrap_or(serde_json::Value::Null)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[napi(getter)]
|
|
190
|
+
pub fn hascustomalias(&self) -> bool {
|
|
191
|
+
self.inner.node.hascustomalias
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[napi(getter)]
|
|
195
|
+
pub fn parentproperty(&self) -> Option<String> {
|
|
196
|
+
self.inner.node.parentproperty.clone()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[napi(getter)]
|
|
200
|
+
pub fn description(&self) -> serde_json::Value {
|
|
201
|
+
serde_json::to_value(&self.inner.node.description).unwrap_or(serde_json::Value::Null)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[napi(getter)]
|
|
205
|
+
pub fn name(&self) -> serde_json::Value {
|
|
206
|
+
serde_json::to_value(&self.inner.node.name).unwrap_or(serde_json::Value::Null)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// -- Tile data getters --
|
|
210
|
+
|
|
211
|
+
#[napi(getter)]
|
|
212
|
+
pub fn tile_id(&self) -> Option<String> {
|
|
213
|
+
self.inner.tile.as_ref().and_then(|t| t.tileid.clone())
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[napi(getter)]
|
|
217
|
+
pub fn tile_data(&self) -> serde_json::Value {
|
|
218
|
+
self.inner
|
|
219
|
+
.tile_data
|
|
220
|
+
.clone()
|
|
221
|
+
.unwrap_or(serde_json::Value::Null)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[napi(getter)]
|
|
225
|
+
pub fn value_loaded(&self) -> bool {
|
|
226
|
+
self.inner.tile.is_some()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[napi]
|
|
230
|
+
pub fn has_tile_data(&self) -> bool {
|
|
231
|
+
self.inner.tile_data.is_some()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[napi]
|
|
235
|
+
pub fn set_tile_data(&mut self, value: serde_json::Value) {
|
|
236
|
+
self.inner.tile_data = Some(value);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// -- Relationship getters --
|
|
240
|
+
|
|
241
|
+
#[napi(getter)]
|
|
242
|
+
pub fn child_node_aliases(&self) -> serde_json::Value {
|
|
243
|
+
serde_json::to_value(&self.inner.child_node_ids).unwrap_or(serde_json::Value::Null)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[napi]
|
|
247
|
+
pub fn get_child_node_id(&self, index: u32) -> Option<String> {
|
|
248
|
+
self.inner.child_node_ids.get(index as usize).cloned()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#[napi]
|
|
252
|
+
pub fn is_iterable(&self) -> bool {
|
|
253
|
+
alizarin_core::is_iterable_datatype(&self.inner.node.datatype)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Get sortorder from the tile
|
|
257
|
+
#[napi(getter)]
|
|
258
|
+
pub fn sortorder(&self) -> serde_json::Value {
|
|
259
|
+
self.inner
|
|
260
|
+
.tile
|
|
261
|
+
.as_ref()
|
|
262
|
+
.and_then(|t| t.sortorder)
|
|
263
|
+
.map(|s| serde_json::Value::Number(s.into()))
|
|
264
|
+
.unwrap_or(serde_json::Value::Null)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// Snapshot all properties in one call (avoids multiple N-API boundary crossings)
|
|
268
|
+
#[napi]
|
|
269
|
+
pub fn to_snapshot(&self) -> serde_json::Value {
|
|
270
|
+
serde_json::json!({
|
|
271
|
+
"nodeId": self.inner.node.nodeid,
|
|
272
|
+
"alias": self.inner.node.alias,
|
|
273
|
+
"datatype": &self.inner.node.datatype,
|
|
274
|
+
"nodegroupId": self.inner.node.nodegroup_id,
|
|
275
|
+
"isCollector": self.inner.is_collector,
|
|
276
|
+
"independent": self.inner.independent,
|
|
277
|
+
"exportable": self.inner.node.exportable,
|
|
278
|
+
"isrequired": self.inner.node.isrequired,
|
|
279
|
+
"issearchable": self.inner.node.issearchable,
|
|
280
|
+
"hascustomalias": self.inner.node.hascustomalias,
|
|
281
|
+
"tileId": self.inner.tile.as_ref().and_then(|t| t.tileid.clone()),
|
|
282
|
+
"tileData": &self.inner.tile_data,
|
|
283
|
+
"valueLoaded": self.inner.tile.is_some(),
|
|
284
|
+
"childNodeIds": &self.inner.child_node_ids,
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// Clear tile data
|
|
289
|
+
#[napi]
|
|
290
|
+
pub fn clear(&mut self) {
|
|
291
|
+
self.inner.tile_data = None;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// =============================================================================
|
|
296
|
+
// NapiPseudoList
|
|
297
|
+
// =============================================================================
|
|
298
|
+
|
|
299
|
+
#[napi]
|
|
300
|
+
pub struct NapiPseudoList {
|
|
301
|
+
inner: PseudoListCore,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[napi]
|
|
305
|
+
impl NapiPseudoList {
|
|
306
|
+
#[napi(getter)]
|
|
307
|
+
pub fn node_alias(&self) -> String {
|
|
308
|
+
self.inner.node_alias.clone()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#[napi(getter)]
|
|
312
|
+
pub fn total_values(&self) -> u32 {
|
|
313
|
+
self.inner.values.len() as u32
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[napi(getter)]
|
|
317
|
+
pub fn is_loaded(&self) -> bool {
|
|
318
|
+
self.inner.is_loaded
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[napi(getter)]
|
|
322
|
+
pub fn is_single(&self) -> bool {
|
|
323
|
+
self.inner.is_single
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[napi]
|
|
327
|
+
pub fn is_iterable(&self) -> bool {
|
|
328
|
+
!self.inner.is_single
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#[napi]
|
|
332
|
+
pub fn get_value(&self, value_index: u32) -> Option<NapiPseudoValue> {
|
|
333
|
+
self.inner
|
|
334
|
+
.values
|
|
335
|
+
.get(value_index as usize)
|
|
336
|
+
.map(|v| NapiPseudoValue { inner: v.clone() })
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[napi]
|
|
340
|
+
pub fn get_all_values(&self) -> Vec<NapiPseudoValue> {
|
|
341
|
+
self.inner
|
|
342
|
+
.values
|
|
343
|
+
.iter()
|
|
344
|
+
.map(|v| NapiPseudoValue { inner: v.clone() })
|
|
345
|
+
.collect()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// =============================================================================
|
|
350
|
+
// NapiPopulateResult
|
|
351
|
+
// =============================================================================
|
|
352
|
+
|
|
353
|
+
#[napi]
|
|
354
|
+
pub struct NapiPopulateResult {
|
|
355
|
+
values: HashMap<String, PseudoListCore>,
|
|
356
|
+
all_values_map: HashMap<String, Option<bool>>,
|
|
357
|
+
all_nodegroups_map: HashMap<String, bool>,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#[napi]
|
|
361
|
+
impl NapiPopulateResult {
|
|
362
|
+
#[napi]
|
|
363
|
+
pub fn get_value_aliases(&self) -> Vec<String> {
|
|
364
|
+
self.values.keys().cloned().collect()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[napi]
|
|
368
|
+
pub fn get_value(&self, alias: String) -> Option<NapiPseudoList> {
|
|
369
|
+
self.values
|
|
370
|
+
.get(&alias)
|
|
371
|
+
.map(|l| NapiPseudoList { inner: l.clone() })
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/// Get all values as a JSON map (single boundary crossing).
|
|
375
|
+
#[napi]
|
|
376
|
+
pub fn get_all_values(&self) -> serde_json::Value {
|
|
377
|
+
let mut map = serde_json::Map::new();
|
|
378
|
+
for (alias, list) in &self.values {
|
|
379
|
+
map.insert(
|
|
380
|
+
alias.clone(),
|
|
381
|
+
serde_json::json!({
|
|
382
|
+
"nodeAlias": list.node_alias,
|
|
383
|
+
"isSingle": list.is_single,
|
|
384
|
+
"isLoaded": list.is_loaded,
|
|
385
|
+
"totalValues": list.values.len(),
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
serde_json::Value::Object(map)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#[napi(getter)]
|
|
393
|
+
pub fn all_values_map(&self) -> serde_json::Value {
|
|
394
|
+
serde_json::to_value(&self.all_values_map).unwrap_or(serde_json::Value::Null)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#[napi(getter)]
|
|
398
|
+
pub fn all_nodegroups_map(&self) -> serde_json::Value {
|
|
399
|
+
serde_json::to_value(&self.all_nodegroups_map).unwrap_or(serde_json::Value::Null)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// =============================================================================
|
|
404
|
+
// NapiEnsureNodegroupResult
|
|
405
|
+
// =============================================================================
|
|
406
|
+
|
|
407
|
+
#[napi]
|
|
408
|
+
pub struct NapiEnsureNodegroupResult {
|
|
409
|
+
values: HashMap<String, PseudoListCore>,
|
|
410
|
+
implied_nodegroups: Vec<String>,
|
|
411
|
+
all_nodegroups_map: HashMap<String, bool>,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[napi]
|
|
415
|
+
impl NapiEnsureNodegroupResult {
|
|
416
|
+
#[napi]
|
|
417
|
+
pub fn get_value_aliases(&self) -> Vec<String> {
|
|
418
|
+
self.values.keys().cloned().collect()
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
#[napi]
|
|
422
|
+
pub fn get_value(&self, alias: String) -> Option<NapiPseudoList> {
|
|
423
|
+
self.values
|
|
424
|
+
.get(&alias)
|
|
425
|
+
.map(|l| NapiPseudoList { inner: l.clone() })
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#[napi]
|
|
429
|
+
pub fn get_all_values(&self) -> serde_json::Value {
|
|
430
|
+
let mut map = serde_json::Map::new();
|
|
431
|
+
for (alias, list) in &self.values {
|
|
432
|
+
map.insert(
|
|
433
|
+
alias.clone(),
|
|
434
|
+
serde_json::json!({
|
|
435
|
+
"nodeAlias": list.node_alias,
|
|
436
|
+
"isSingle": list.is_single,
|
|
437
|
+
"isLoaded": list.is_loaded,
|
|
438
|
+
"totalValues": list.values.len(),
|
|
439
|
+
}),
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
serde_json::Value::Object(map)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[napi(getter)]
|
|
446
|
+
pub fn implied_nodegroups(&self) -> Vec<String> {
|
|
447
|
+
self.implied_nodegroups.clone()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#[napi(getter)]
|
|
451
|
+
pub fn all_nodegroups_map(&self) -> serde_json::Value {
|
|
452
|
+
serde_json::to_value(&self.all_nodegroups_map).unwrap_or(serde_json::Value::Null)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// =============================================================================
|
|
457
|
+
// NapiValuesFromNodegroupResult
|
|
458
|
+
// =============================================================================
|
|
459
|
+
|
|
460
|
+
#[napi]
|
|
461
|
+
pub struct NapiValuesFromNodegroupResult {
|
|
462
|
+
values: HashMap<String, PseudoListCore>,
|
|
463
|
+
implied_nodegroups: Vec<String>,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
#[napi]
|
|
467
|
+
impl NapiValuesFromNodegroupResult {
|
|
468
|
+
#[napi]
|
|
469
|
+
pub fn get_all_values(&self) -> serde_json::Value {
|
|
470
|
+
let mut map = serde_json::Map::new();
|
|
471
|
+
for (alias, list) in &self.values {
|
|
472
|
+
map.insert(
|
|
473
|
+
alias.clone(),
|
|
474
|
+
serde_json::json!({
|
|
475
|
+
"nodeAlias": list.node_alias,
|
|
476
|
+
"isSingle": list.is_single,
|
|
477
|
+
"isLoaded": list.is_loaded,
|
|
478
|
+
"totalValues": list.values.len(),
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
serde_json::Value::Object(map)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#[napi(getter)]
|
|
486
|
+
pub fn implied_nodegroups(&self) -> Vec<String> {
|
|
487
|
+
self.implied_nodegroups.clone()
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// =============================================================================
|
|
492
|
+
// NapiResourceInstanceWrapper
|
|
493
|
+
// =============================================================================
|
|
494
|
+
|
|
495
|
+
#[napi]
|
|
496
|
+
pub struct NapiResourceInstanceWrapper {
|
|
497
|
+
inner: ResourceInstanceWrapperCore,
|
|
498
|
+
model_access: GraphModelAccess,
|
|
499
|
+
lazy: bool,
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
#[napi]
|
|
503
|
+
impl NapiResourceInstanceWrapper {
|
|
504
|
+
// =========================================================================
|
|
505
|
+
// Construction
|
|
506
|
+
// =========================================================================
|
|
507
|
+
|
|
508
|
+
/// Create a wrapper for a given graph (must be registered).
|
|
509
|
+
#[napi(constructor)]
|
|
510
|
+
pub fn new(graph_id: String) -> Result<Self> {
|
|
511
|
+
let graph = alizarin_core::get_graph(&graph_id).ok_or_else(|| {
|
|
512
|
+
napi::Error::from_reason(format!(
|
|
513
|
+
"Graph '{}' not registered. Call registerGraph() first.",
|
|
514
|
+
graph_id
|
|
515
|
+
))
|
|
516
|
+
})?;
|
|
517
|
+
|
|
518
|
+
let model_access = GraphModelAccess::from_graph(&graph);
|
|
519
|
+
let mut core = ResourceInstanceWrapperCore::new(graph_id);
|
|
520
|
+
core.set_cached_indices(&model_access);
|
|
521
|
+
|
|
522
|
+
Ok(NapiResourceInstanceWrapper {
|
|
523
|
+
inner: core,
|
|
524
|
+
model_access,
|
|
525
|
+
lazy: false,
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// =========================================================================
|
|
530
|
+
// Tile loading
|
|
531
|
+
// =========================================================================
|
|
532
|
+
|
|
533
|
+
/// Load tiles from a JSON array.
|
|
534
|
+
#[napi]
|
|
535
|
+
pub fn load_tiles(&mut self, tiles_js: serde_json::Value) -> Result<()> {
|
|
536
|
+
let tiles: Vec<StaticTile> = serde_json::from_value(tiles_js)
|
|
537
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid tiles JSON: {e}")))?;
|
|
538
|
+
self.inner.load_tiles(tiles);
|
|
539
|
+
Ok(())
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/// Load tiles directly from a StaticResource JSON.
|
|
543
|
+
#[napi]
|
|
544
|
+
pub fn load_tiles_from_resource(&mut self, resource_js: serde_json::Value) -> Result<()> {
|
|
545
|
+
let resource: StaticResource = serde_json::from_value(resource_js)
|
|
546
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid resource JSON: {e}")))?;
|
|
547
|
+
|
|
548
|
+
self.inner.resource_instance = Some(resource.resourceinstance.clone());
|
|
549
|
+
|
|
550
|
+
if let Some(tiles_vec) = resource.tiles {
|
|
551
|
+
self.inner.load_tiles(tiles_vec);
|
|
552
|
+
}
|
|
553
|
+
Ok(())
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/// Append tiles incrementally (for lazy loading).
|
|
557
|
+
#[napi]
|
|
558
|
+
pub fn append_tiles(&mut self, tiles_js: serde_json::Value) -> Result<()> {
|
|
559
|
+
let tiles: Vec<StaticTile> = serde_json::from_value(tiles_js)
|
|
560
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid tiles JSON: {e}")))?;
|
|
561
|
+
|
|
562
|
+
if self.inner.tiles.is_none() {
|
|
563
|
+
self.inner.tiles = Some(HashMap::new());
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let tiles_map = self.inner.tiles.as_mut().unwrap();
|
|
567
|
+
for tile in tiles {
|
|
568
|
+
let tile_id = tile
|
|
569
|
+
.tileid
|
|
570
|
+
.clone()
|
|
571
|
+
.unwrap_or_else(|| format!("synthetic_{}", tiles_map.len()));
|
|
572
|
+
self.inner
|
|
573
|
+
.nodegroup_index
|
|
574
|
+
.entry(tile.nodegroup_id.clone())
|
|
575
|
+
.or_default()
|
|
576
|
+
.push(tile_id.clone());
|
|
577
|
+
tiles_map.insert(tile_id, tile);
|
|
578
|
+
}
|
|
579
|
+
Ok(())
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
#[napi]
|
|
583
|
+
pub fn get_tile_count(&self) -> u32 {
|
|
584
|
+
self.inner.tiles.as_ref().map(|t| t.len()).unwrap_or(0) as u32
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/// Export all tiles as a JSON string.
|
|
588
|
+
/// Returns the wrapper's current tile state (including any mutations from setTileDataForNode).
|
|
589
|
+
/// Returned as a string for fast boundary crossing — call JSON.parse() on the JS side.
|
|
590
|
+
#[napi]
|
|
591
|
+
pub fn export_tiles_json(&self) -> Result<String> {
|
|
592
|
+
let tiles: Vec<&StaticTile> = self
|
|
593
|
+
.inner
|
|
594
|
+
.tiles
|
|
595
|
+
.as_ref()
|
|
596
|
+
.map(|t| t.values().collect())
|
|
597
|
+
.unwrap_or_default();
|
|
598
|
+
serde_json::to_string(&tiles).map_err(|e| napi::Error::from_reason(e.to_string()))
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/// Prune tiles to only keep those in permitted nodegroups.
|
|
602
|
+
#[napi(js_name = "pruneResourceTiles")]
|
|
603
|
+
pub fn prune_resource_tiles(&mut self) -> Result<()> {
|
|
604
|
+
let tiles = self
|
|
605
|
+
.inner
|
|
606
|
+
.tiles
|
|
607
|
+
.take()
|
|
608
|
+
.ok_or_else(|| napi::Error::from_reason("Tiles not initialized".to_string()))?;
|
|
609
|
+
let pruned: HashMap<String, StaticTile> = tiles
|
|
610
|
+
.into_iter()
|
|
611
|
+
.filter(|(_id, tile)| self.model_access.is_nodegroup_permitted(&tile.nodegroup_id))
|
|
612
|
+
.collect();
|
|
613
|
+
self.inner.tiles = Some(pruned);
|
|
614
|
+
Ok(())
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#[napi]
|
|
618
|
+
pub fn tiles_loaded(&self) -> bool {
|
|
619
|
+
self.inner
|
|
620
|
+
.tiles
|
|
621
|
+
.as_ref()
|
|
622
|
+
.map(|t| !self.lazy || !t.is_empty())
|
|
623
|
+
.unwrap_or(false)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
#[napi]
|
|
627
|
+
pub fn get_all_tile_ids(&self) -> Vec<String> {
|
|
628
|
+
self.inner
|
|
629
|
+
.tiles
|
|
630
|
+
.as_ref()
|
|
631
|
+
.map(|t| t.keys().cloned().collect())
|
|
632
|
+
.unwrap_or_default()
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
#[napi]
|
|
636
|
+
pub fn get_tile_ids_by_nodegroup(&self, nodegroup_id: Option<String>) -> Vec<String> {
|
|
637
|
+
match nodegroup_id {
|
|
638
|
+
Some(ng_id) => self
|
|
639
|
+
.inner
|
|
640
|
+
.nodegroup_index
|
|
641
|
+
.get(&ng_id)
|
|
642
|
+
.cloned()
|
|
643
|
+
.unwrap_or_default(),
|
|
644
|
+
None => Vec::new(),
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
#[napi]
|
|
649
|
+
pub fn get_tile(&self, tile_id: String) -> Result<serde_json::Value> {
|
|
650
|
+
let tile = self
|
|
651
|
+
.inner
|
|
652
|
+
.get_tile(&tile_id)
|
|
653
|
+
.ok_or_else(|| napi::Error::from_reason(format!("Tile '{}' not found", tile_id)))?;
|
|
654
|
+
serde_json::to_value(tile).map_err(|e| napi::Error::from_reason(e.to_string()))
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#[napi]
|
|
658
|
+
pub fn get_tile_data(&self, tile_id: String, node_id: String) -> serde_json::Value {
|
|
659
|
+
self.inner
|
|
660
|
+
.get_tile(&tile_id)
|
|
661
|
+
.and_then(|t| t.data.get(&node_id).cloned())
|
|
662
|
+
.unwrap_or(serde_json::Value::Null)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/// Set a single node's data in a tile, mutating in place.
|
|
666
|
+
/// Returns true if the tile was found and updated.
|
|
667
|
+
#[napi]
|
|
668
|
+
pub fn set_tile_data_for_node(
|
|
669
|
+
&mut self,
|
|
670
|
+
tile_id: String,
|
|
671
|
+
node_id: String,
|
|
672
|
+
value: serde_json::Value,
|
|
673
|
+
) -> bool {
|
|
674
|
+
self.inner.set_tile_data_for_node(&tile_id, &node_id, value)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#[napi]
|
|
678
|
+
pub fn has_tiles_for_nodegroup(&self, nodegroup_id: String) -> bool {
|
|
679
|
+
if self.lazy {
|
|
680
|
+
self.inner.is_nodegroup_loaded(&nodegroup_id)
|
|
681
|
+
} else {
|
|
682
|
+
self.inner
|
|
683
|
+
.tiles
|
|
684
|
+
.as_ref()
|
|
685
|
+
.map(|t| !t.is_empty())
|
|
686
|
+
.unwrap_or(false)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
#[napi]
|
|
691
|
+
pub fn get_nodegroup_count(&self) -> u32 {
|
|
692
|
+
self.inner.nodegroup_index.len() as u32
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// =========================================================================
|
|
696
|
+
// Lazy loading state
|
|
697
|
+
// =========================================================================
|
|
698
|
+
|
|
699
|
+
#[napi]
|
|
700
|
+
pub fn set_lazy(&mut self, lazy: bool) {
|
|
701
|
+
self.lazy = lazy;
|
|
702
|
+
if lazy && self.inner.tiles.is_none() {
|
|
703
|
+
self.inner.tiles = Some(HashMap::new());
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
#[napi]
|
|
708
|
+
pub fn is_nodegroup_loaded(&self, nodegroup_id: Option<String>) -> bool {
|
|
709
|
+
match nodegroup_id {
|
|
710
|
+
Some(id) => self.inner.is_nodegroup_loaded(&id),
|
|
711
|
+
None => false,
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
#[napi]
|
|
716
|
+
pub fn is_nodegroup_loaded_or_loading(&self, nodegroup_id: Option<String>) -> bool {
|
|
717
|
+
match nodegroup_id {
|
|
718
|
+
Some(id) => {
|
|
719
|
+
if let Ok(loaded) = self.inner.loaded_nodegroups.lock() {
|
|
720
|
+
matches!(
|
|
721
|
+
loaded.get(&id),
|
|
722
|
+
Some(LoadState::Loaded) | Some(LoadState::Loading)
|
|
723
|
+
)
|
|
724
|
+
} else {
|
|
725
|
+
false
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
None => false,
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#[napi]
|
|
733
|
+
pub fn try_acquire_nodegroup_lock(&self, nodegroup_id: String) -> bool {
|
|
734
|
+
if let Ok(mut loaded) = self.inner.loaded_nodegroups.lock() {
|
|
735
|
+
let state = loaded.entry(nodegroup_id).or_insert(LoadState::NotLoaded);
|
|
736
|
+
if *state == LoadState::NotLoaded {
|
|
737
|
+
*state = LoadState::Loading;
|
|
738
|
+
true
|
|
739
|
+
} else {
|
|
740
|
+
false
|
|
741
|
+
}
|
|
742
|
+
} else {
|
|
743
|
+
false
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
#[napi]
|
|
748
|
+
pub fn get_missing_nodegroups_for_children(&self, parent_node_id: String) -> Vec<String> {
|
|
749
|
+
let child_nodes = self.model_access.get_child_nodes(&parent_node_id);
|
|
750
|
+
|
|
751
|
+
if !self.lazy {
|
|
752
|
+
return Vec::new();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
let mut missing = Vec::new();
|
|
756
|
+
for child in child_nodes.values() {
|
|
757
|
+
if let Some(ng_id) = &child.nodegroup_id {
|
|
758
|
+
if !self.inner.is_nodegroup_loaded(ng_id) {
|
|
759
|
+
missing.push(ng_id.to_string());
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
missing
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// =========================================================================
|
|
767
|
+
// Resource metadata
|
|
768
|
+
// =========================================================================
|
|
769
|
+
|
|
770
|
+
#[napi]
|
|
771
|
+
pub fn get_resource_id(&self) -> Option<String> {
|
|
772
|
+
self.inner
|
|
773
|
+
.resource_instance
|
|
774
|
+
.as_ref()
|
|
775
|
+
.map(|ri| ri.resourceinstanceid.clone())
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
#[napi]
|
|
779
|
+
pub fn get_name(&self) -> Result<serde_json::Value> {
|
|
780
|
+
match self.inner.resource_instance.as_ref() {
|
|
781
|
+
Some(ri) => serde_json::to_value(&ri.name)
|
|
782
|
+
.map_err(|e| napi::Error::from_reason(format!("Serialization failed: {}", e))),
|
|
783
|
+
None => Ok(serde_json::Value::Null),
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
#[napi]
|
|
788
|
+
pub fn get_descriptors(&self, _recompute: bool) -> Result<serde_json::Value> {
|
|
789
|
+
// For now return cached descriptors; recompute=true could be supported later
|
|
790
|
+
match self.inner.resource_instance.as_ref() {
|
|
791
|
+
Some(ri) => serde_json::to_value(&ri.descriptors)
|
|
792
|
+
.map_err(|e| napi::Error::from_reason(format!("Serialization failed: {}", e))),
|
|
793
|
+
None => Ok(serde_json::Value::Null),
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// =========================================================================
|
|
798
|
+
// Populate & tree building
|
|
799
|
+
// =========================================================================
|
|
800
|
+
|
|
801
|
+
/// Populate the pseudo cache for the given nodegroups.
|
|
802
|
+
#[napi]
|
|
803
|
+
pub fn populate(
|
|
804
|
+
&self,
|
|
805
|
+
lazy: bool,
|
|
806
|
+
nodegroup_ids: Vec<String>,
|
|
807
|
+
root_node_alias: String,
|
|
808
|
+
) -> Result<NapiPopulateResult> {
|
|
809
|
+
let result = self
|
|
810
|
+
.inner
|
|
811
|
+
.populate(lazy, &nodegroup_ids, &root_node_alias, &self.model_access)
|
|
812
|
+
.map_err(sc_err)?;
|
|
813
|
+
|
|
814
|
+
Ok(NapiPopulateResult {
|
|
815
|
+
values: result.values,
|
|
816
|
+
all_values_map: result.all_values_map,
|
|
817
|
+
all_nodegroups_map: result.all_nodegroups_map,
|
|
818
|
+
})
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/// Ensure a single nodegroup is loaded and return structured values.
|
|
822
|
+
#[napi]
|
|
823
|
+
pub fn ensure_nodegroup(
|
|
824
|
+
&self,
|
|
825
|
+
all_values_js: serde_json::Value,
|
|
826
|
+
all_nodegroups_js: serde_json::Value,
|
|
827
|
+
nodegroup_id: String,
|
|
828
|
+
add_if_missing: bool,
|
|
829
|
+
nodegroup_permissions_js: serde_json::Value,
|
|
830
|
+
do_implied_nodegroups: bool,
|
|
831
|
+
) -> Result<NapiEnsureNodegroupResult> {
|
|
832
|
+
let all_values: HashMap<String, Option<bool>> = serde_json::from_value(all_values_js)
|
|
833
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid all_values: {e}")))?;
|
|
834
|
+
let mut all_nodegroups: HashMap<String, bool> =
|
|
835
|
+
serde_json::from_value(all_nodegroups_js)
|
|
836
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid all_nodegroups: {e}")))?;
|
|
837
|
+
// PermissionRule is currently used only for tile filtering, and may not be
|
|
838
|
+
// JSON-serializable. Use the model's permissions if available.
|
|
839
|
+
let _ = nodegroup_permissions_js;
|
|
840
|
+
let nodegroup_permissions = self.model_access.get_permitted_nodegroups();
|
|
841
|
+
|
|
842
|
+
let result = self
|
|
843
|
+
.inner
|
|
844
|
+
.ensure_nodegroup(
|
|
845
|
+
&all_values,
|
|
846
|
+
&mut all_nodegroups,
|
|
847
|
+
&nodegroup_id,
|
|
848
|
+
add_if_missing,
|
|
849
|
+
&nodegroup_permissions,
|
|
850
|
+
do_implied_nodegroups,
|
|
851
|
+
&self.model_access,
|
|
852
|
+
)
|
|
853
|
+
.map_err(sc_err)?;
|
|
854
|
+
|
|
855
|
+
Ok(NapiEnsureNodegroupResult {
|
|
856
|
+
values: result.values,
|
|
857
|
+
implied_nodegroups: result.implied_nodegroups,
|
|
858
|
+
all_nodegroups_map: result.all_nodegroups_map,
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/// Build pseudo values from tiles for a specific nodegroup.
|
|
863
|
+
#[napi]
|
|
864
|
+
pub fn values_from_resource_nodegroup(
|
|
865
|
+
&self,
|
|
866
|
+
existing_values_js: serde_json::Value,
|
|
867
|
+
nodegroup_tile_ids: Vec<String>,
|
|
868
|
+
nodegroup_id: String,
|
|
869
|
+
) -> Result<NapiValuesFromNodegroupResult> {
|
|
870
|
+
let existing_values: HashMap<String, Option<bool>> =
|
|
871
|
+
serde_json::from_value(existing_values_js)
|
|
872
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid existing_values: {e}")))?;
|
|
873
|
+
|
|
874
|
+
let result = self
|
|
875
|
+
.inner
|
|
876
|
+
.values_from_resource_nodegroup(
|
|
877
|
+
&existing_values,
|
|
878
|
+
&nodegroup_tile_ids,
|
|
879
|
+
&nodegroup_id,
|
|
880
|
+
&self.model_access,
|
|
881
|
+
)
|
|
882
|
+
.map_err(sc_err)?;
|
|
883
|
+
|
|
884
|
+
Ok(NapiValuesFromNodegroupResult {
|
|
885
|
+
values: result.values,
|
|
886
|
+
implied_nodegroups: result.implied_nodegroups,
|
|
887
|
+
})
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// =========================================================================
|
|
891
|
+
// Path resolution
|
|
892
|
+
// =========================================================================
|
|
893
|
+
|
|
894
|
+
/// Resolve a dot-separated path and return a PseudoList.
|
|
895
|
+
#[napi]
|
|
896
|
+
pub fn get_values_at_path(&self, path: String) -> Result<NapiPseudoList> {
|
|
897
|
+
let result = self
|
|
898
|
+
.inner
|
|
899
|
+
.get_values_at_path(&path, &self.model_access, None)
|
|
900
|
+
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
|
901
|
+
Ok(NapiPseudoList { inner: result })
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// =========================================================================
|
|
905
|
+
// Semantic child traversal
|
|
906
|
+
// =========================================================================
|
|
907
|
+
|
|
908
|
+
/// Get semantic child value for a parent/child relationship.
|
|
909
|
+
#[napi]
|
|
910
|
+
pub fn get_semantic_child_value(
|
|
911
|
+
&self,
|
|
912
|
+
parent_tile_id: Option<String>,
|
|
913
|
+
parent_node_id: String,
|
|
914
|
+
parent_nodegroup_id: Option<String>,
|
|
915
|
+
child_alias: String,
|
|
916
|
+
) -> Result<serde_json::Value> {
|
|
917
|
+
let result = self
|
|
918
|
+
.inner
|
|
919
|
+
.get_semantic_child_value(
|
|
920
|
+
parent_tile_id.as_ref(),
|
|
921
|
+
&parent_node_id,
|
|
922
|
+
parent_nodegroup_id.as_ref(),
|
|
923
|
+
&child_alias,
|
|
924
|
+
&self.model_access,
|
|
925
|
+
)
|
|
926
|
+
.map_err(sc_err)?;
|
|
927
|
+
|
|
928
|
+
match result {
|
|
929
|
+
SemanticChildResult::List(list) => {
|
|
930
|
+
Ok(serde_json::to_value(NapiPseudoListJson::from(&list))
|
|
931
|
+
.unwrap_or(serde_json::Value::Null))
|
|
932
|
+
}
|
|
933
|
+
SemanticChildResult::Single(val) => {
|
|
934
|
+
Ok(serde_json::to_value(NapiPseudoValueJson::from(&val))
|
|
935
|
+
.unwrap_or(serde_json::Value::Null))
|
|
936
|
+
}
|
|
937
|
+
SemanticChildResult::Empty => Ok(serde_json::Value::Null),
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/// Get all semantic child values for a parent node.
|
|
942
|
+
#[napi]
|
|
943
|
+
pub fn get_all_semantic_child_values(
|
|
944
|
+
&self,
|
|
945
|
+
parent_tile_id: Option<String>,
|
|
946
|
+
parent_node_id: String,
|
|
947
|
+
parent_nodegroup_id: Option<String>,
|
|
948
|
+
) -> Result<serde_json::Value> {
|
|
949
|
+
// Get child nodes
|
|
950
|
+
let child_nodes = self.model_access.get_child_nodes(&parent_node_id);
|
|
951
|
+
|
|
952
|
+
let mut results = serde_json::Map::new();
|
|
953
|
+
for child in child_nodes.values() {
|
|
954
|
+
if let Some(ref alias) = child.alias {
|
|
955
|
+
let result = self.inner.get_semantic_child_value(
|
|
956
|
+
parent_tile_id.as_ref(),
|
|
957
|
+
&parent_node_id,
|
|
958
|
+
parent_nodegroup_id.as_ref(),
|
|
959
|
+
alias,
|
|
960
|
+
&self.model_access,
|
|
961
|
+
);
|
|
962
|
+
match result {
|
|
963
|
+
Ok(SemanticChildResult::List(list)) => {
|
|
964
|
+
results.insert(
|
|
965
|
+
alias.clone(),
|
|
966
|
+
serde_json::to_value(NapiPseudoListJson::from(&list))
|
|
967
|
+
.unwrap_or(serde_json::Value::Null),
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
Ok(SemanticChildResult::Single(val)) => {
|
|
971
|
+
results.insert(
|
|
972
|
+
alias.clone(),
|
|
973
|
+
serde_json::to_value(NapiPseudoValueJson::from(&val))
|
|
974
|
+
.unwrap_or(serde_json::Value::Null),
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
Ok(SemanticChildResult::Empty) => {
|
|
978
|
+
results.insert(alias.clone(), serde_json::Value::Null);
|
|
979
|
+
}
|
|
980
|
+
Err(_) => {
|
|
981
|
+
results.insert(alias.clone(), serde_json::Value::Null);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
Ok(serde_json::Value::Object(results))
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/// Find all semantic children of a parent node (returns map of alias → tile IDs).
|
|
990
|
+
#[napi]
|
|
991
|
+
pub fn find_semantic_children(
|
|
992
|
+
&self,
|
|
993
|
+
parent_tile_id: Option<String>,
|
|
994
|
+
parent_node_id: String,
|
|
995
|
+
parent_nodegroup_id: Option<String>,
|
|
996
|
+
) -> Result<serde_json::Value> {
|
|
997
|
+
let child_nodes = self.model_access.get_child_nodes(&parent_node_id);
|
|
998
|
+
|
|
999
|
+
let tiles = self
|
|
1000
|
+
.inner
|
|
1001
|
+
.tiles
|
|
1002
|
+
.as_ref()
|
|
1003
|
+
.ok_or_else(|| napi::Error::from_reason("Tiles not initialized"))?;
|
|
1004
|
+
|
|
1005
|
+
let mut results = serde_json::Map::new();
|
|
1006
|
+
for child in child_nodes.values() {
|
|
1007
|
+
if let Some(ref alias) = child.alias {
|
|
1008
|
+
let matching_tile_ids: Vec<String> = tiles
|
|
1009
|
+
.iter()
|
|
1010
|
+
.filter(|(_, tile)| {
|
|
1011
|
+
alizarin_core::matches_semantic_child(
|
|
1012
|
+
parent_tile_id.as_ref(),
|
|
1013
|
+
parent_nodegroup_id.as_ref(),
|
|
1014
|
+
child,
|
|
1015
|
+
tile,
|
|
1016
|
+
)
|
|
1017
|
+
})
|
|
1018
|
+
.filter_map(|(_, tile)| tile.tileid.clone())
|
|
1019
|
+
.collect();
|
|
1020
|
+
results.insert(
|
|
1021
|
+
alias.clone(),
|
|
1022
|
+
serde_json::to_value(&matching_tile_ids).unwrap(),
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
Ok(serde_json::Value::Object(results))
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// =========================================================================
|
|
1030
|
+
// Pseudo cache management
|
|
1031
|
+
// =========================================================================
|
|
1032
|
+
|
|
1033
|
+
#[napi]
|
|
1034
|
+
pub fn get_cached_pseudo(&self, alias: Option<String>) -> Option<NapiPseudoList> {
|
|
1035
|
+
let alias = alias?;
|
|
1036
|
+
self.inner
|
|
1037
|
+
.get_cached_pseudo(&alias)
|
|
1038
|
+
.map(|l| NapiPseudoList { inner: l })
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
#[napi]
|
|
1042
|
+
pub fn get_root_pseudo(&self) -> Option<NapiPseudoValue> {
|
|
1043
|
+
let root = self.model_access.get_root_node().ok()?;
|
|
1044
|
+
let alias = root.alias.as_deref()?;
|
|
1045
|
+
let cached = self.inner.get_cached_pseudo(alias)?;
|
|
1046
|
+
cached
|
|
1047
|
+
.values
|
|
1048
|
+
.first()
|
|
1049
|
+
.map(|v| NapiPseudoValue { inner: v.clone() })
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
#[napi]
|
|
1053
|
+
pub fn cache_pseudo_list(&self, alias: String, list: &NapiPseudoList) {
|
|
1054
|
+
self.inner.store_pseudo(alias, list.inner.clone());
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
#[napi]
|
|
1058
|
+
pub fn cache_pseudo_value(&self, alias: String, value: &NapiPseudoValue) {
|
|
1059
|
+
let list = PseudoListCore::from_values_with_cardinality(
|
|
1060
|
+
alias.clone(),
|
|
1061
|
+
vec![value.inner.clone()],
|
|
1062
|
+
true,
|
|
1063
|
+
);
|
|
1064
|
+
self.inner.store_pseudo(alias, list);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
#[napi]
|
|
1068
|
+
pub fn clear_pseudo_cache(&self) {
|
|
1069
|
+
if let Ok(mut cache) = self.inner.pseudo_cache.lock() {
|
|
1070
|
+
cache.clear();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/// Construct a PseudoValue from node metadata (for TS wrapper).
|
|
1075
|
+
#[napi]
|
|
1076
|
+
pub fn make_pseudo_value(
|
|
1077
|
+
&self,
|
|
1078
|
+
alias: String,
|
|
1079
|
+
tile_id: Option<String>,
|
|
1080
|
+
is_permitted: bool,
|
|
1081
|
+
is_single: bool,
|
|
1082
|
+
) -> Result<serde_json::Value> {
|
|
1083
|
+
// Find node by alias
|
|
1084
|
+
let nodes = self
|
|
1085
|
+
.model_access
|
|
1086
|
+
.get_nodes()
|
|
1087
|
+
.ok_or_else(|| napi::Error::from_reason("Nodes not initialized"))?;
|
|
1088
|
+
|
|
1089
|
+
let node = nodes
|
|
1090
|
+
.values()
|
|
1091
|
+
.find(|n| n.alias.as_deref() == Some(&alias))
|
|
1092
|
+
.ok_or_else(|| {
|
|
1093
|
+
napi::Error::from_reason(format!("Node with alias '{}' not found", alias))
|
|
1094
|
+
})?;
|
|
1095
|
+
|
|
1096
|
+
let edges = self
|
|
1097
|
+
.model_access
|
|
1098
|
+
.get_edges()
|
|
1099
|
+
.ok_or_else(|| napi::Error::from_reason("Edges not initialized"))?;
|
|
1100
|
+
let child_node_ids = edges.get(&node.nodeid).cloned().unwrap_or_default();
|
|
1101
|
+
|
|
1102
|
+
// Get tile if tile_id provided
|
|
1103
|
+
let tile = tile_id
|
|
1104
|
+
.as_ref()
|
|
1105
|
+
.and_then(|tid| self.inner.get_tile(tid))
|
|
1106
|
+
.map(|t| Arc::new(t.clone()));
|
|
1107
|
+
|
|
1108
|
+
let tile_data = tile
|
|
1109
|
+
.as_ref()
|
|
1110
|
+
.and_then(|t| t.data.get(&node.nodeid).cloned());
|
|
1111
|
+
|
|
1112
|
+
let value =
|
|
1113
|
+
PseudoValueCore::from_node_and_tile(Arc::clone(node), tile, tile_data, child_node_ids);
|
|
1114
|
+
|
|
1115
|
+
// Return as JSON with metadata about how to wrap it
|
|
1116
|
+
Ok(serde_json::json!({
|
|
1117
|
+
"alias": alias,
|
|
1118
|
+
"isSingle": is_single,
|
|
1119
|
+
"isPermitted": is_permitted,
|
|
1120
|
+
"value": NapiPseudoValueJson::from(&value),
|
|
1121
|
+
}))
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// =========================================================================
|
|
1125
|
+
// JSON serialization
|
|
1126
|
+
// =========================================================================
|
|
1127
|
+
|
|
1128
|
+
/// Serialize to JSON (tile_data mode — raw values).
|
|
1129
|
+
#[napi]
|
|
1130
|
+
pub fn to_json(&self) -> Result<serde_json::Value> {
|
|
1131
|
+
self.serialize_with_options(SerializationOptions::tile_data(), None, None, None)
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/// Serialize to display JSON (resolved labels).
|
|
1135
|
+
#[napi]
|
|
1136
|
+
pub fn to_display_json(
|
|
1137
|
+
&self,
|
|
1138
|
+
rdm_cache: &NapiRdmCache,
|
|
1139
|
+
node_config_manager: &NapiNodeConfigManager,
|
|
1140
|
+
language: Option<String>,
|
|
1141
|
+
) -> Result<serde_json::Value> {
|
|
1142
|
+
let lang = language.unwrap_or_else(|| "en".to_string());
|
|
1143
|
+
self.serialize_with_options(
|
|
1144
|
+
SerializationOptions::display(&lang),
|
|
1145
|
+
Some(&rdm_cache.inner),
|
|
1146
|
+
Some(&node_config_manager.inner),
|
|
1147
|
+
None,
|
|
1148
|
+
)
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/// Serialize to display JSON without RDM/config (basic labels only).
|
|
1152
|
+
#[napi]
|
|
1153
|
+
pub fn to_display_json_simple(&self, language: Option<String>) -> Result<serde_json::Value> {
|
|
1154
|
+
let lang = language.unwrap_or_else(|| "en".to_string());
|
|
1155
|
+
self.serialize_with_options(SerializationOptions::display(&lang), None, None, None)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// =========================================================================
|
|
1159
|
+
// Cleanup
|
|
1160
|
+
// =========================================================================
|
|
1161
|
+
|
|
1162
|
+
#[napi]
|
|
1163
|
+
pub fn release(&mut self) {
|
|
1164
|
+
self.inner.tiles = None;
|
|
1165
|
+
self.inner.nodegroup_index.clear();
|
|
1166
|
+
self.clear_pseudo_cache();
|
|
1167
|
+
if let Ok(mut loaded) = self.inner.loaded_nodegroups.lock() {
|
|
1168
|
+
loaded.clear();
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// =============================================================================
|
|
1174
|
+
// Private helpers
|
|
1175
|
+
// =============================================================================
|
|
1176
|
+
|
|
1177
|
+
impl NapiResourceInstanceWrapper {
|
|
1178
|
+
fn serialize_with_options(
|
|
1179
|
+
&self,
|
|
1180
|
+
options: SerializationOptions,
|
|
1181
|
+
rdm: Option<&RdmCache>,
|
|
1182
|
+
ncm: Option<&NodeConfigManager>,
|
|
1183
|
+
resource_registry: Option<&alizarin_core::StaticResourceRegistry>,
|
|
1184
|
+
) -> Result<serde_json::Value> {
|
|
1185
|
+
let cache = self
|
|
1186
|
+
.inner
|
|
1187
|
+
.pseudo_cache
|
|
1188
|
+
.lock()
|
|
1189
|
+
.map_err(|_| napi::Error::from_reason("Failed to lock pseudo cache"))?;
|
|
1190
|
+
|
|
1191
|
+
// Get root alias
|
|
1192
|
+
let root_node = self
|
|
1193
|
+
.model_access
|
|
1194
|
+
.get_root_node()
|
|
1195
|
+
.map_err(napi::Error::from_reason)?;
|
|
1196
|
+
let root_alias = root_node
|
|
1197
|
+
.alias
|
|
1198
|
+
.as_deref()
|
|
1199
|
+
.ok_or_else(|| napi::Error::from_reason("Root node has no alias"))?;
|
|
1200
|
+
|
|
1201
|
+
let root_list = cache.get(root_alias).ok_or_else(|| {
|
|
1202
|
+
napi::Error::from_reason("Root pseudo not found — call populate() first")
|
|
1203
|
+
})?;
|
|
1204
|
+
|
|
1205
|
+
// Build graph indices for VisitorContext
|
|
1206
|
+
let nodes_by_alias = self
|
|
1207
|
+
.model_access
|
|
1208
|
+
.get_nodes_by_alias_arc()
|
|
1209
|
+
.ok_or_else(|| napi::Error::from_reason("Nodes by alias not built"))?;
|
|
1210
|
+
let edges = self
|
|
1211
|
+
.model_access
|
|
1212
|
+
.get_edges_arc()
|
|
1213
|
+
.ok_or_else(|| napi::Error::from_reason("Edges not built"))?;
|
|
1214
|
+
|
|
1215
|
+
let ext_reg = ext_registry();
|
|
1216
|
+
|
|
1217
|
+
let ser_ctx = SerializationContext {
|
|
1218
|
+
node_config: None,
|
|
1219
|
+
external_resolver: rdm
|
|
1220
|
+
.map(|r| r as &dyn alizarin_core::type_serialization::ExternalResolver),
|
|
1221
|
+
resource_resolver: resource_registry
|
|
1222
|
+
.map(|r| r as &dyn alizarin_core::type_serialization::ResourceDisplayResolver),
|
|
1223
|
+
extension_registry: Some(ext_reg),
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
let ctx = VisitorContext {
|
|
1227
|
+
pseudo_cache: &*cache,
|
|
1228
|
+
nodes_by_alias: &nodes_by_alias,
|
|
1229
|
+
edges: &edges,
|
|
1230
|
+
depth: 0,
|
|
1231
|
+
max_depth: 50,
|
|
1232
|
+
serialization_options: options,
|
|
1233
|
+
serialization_context: ser_ctx,
|
|
1234
|
+
node_config_manager: ncm,
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
Ok(root_list.to_json(&ctx))
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// =============================================================================
|
|
1242
|
+
// JSON serialization helpers for SemanticChildResult
|
|
1243
|
+
// =============================================================================
|
|
1244
|
+
|
|
1245
|
+
#[derive(serde::Serialize)]
|
|
1246
|
+
struct NapiPseudoValueJson {
|
|
1247
|
+
node_id: String,
|
|
1248
|
+
alias: Option<String>,
|
|
1249
|
+
datatype: String,
|
|
1250
|
+
nodegroup_id: Option<String>,
|
|
1251
|
+
tile_id: Option<String>,
|
|
1252
|
+
tile_data: Option<serde_json::Value>,
|
|
1253
|
+
is_collector: bool,
|
|
1254
|
+
independent: bool,
|
|
1255
|
+
child_node_ids: Vec<String>,
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
impl From<&PseudoValueCore> for NapiPseudoValueJson {
|
|
1259
|
+
fn from(v: &PseudoValueCore) -> Self {
|
|
1260
|
+
NapiPseudoValueJson {
|
|
1261
|
+
node_id: v.node.nodeid.clone(),
|
|
1262
|
+
alias: v.node.alias.clone(),
|
|
1263
|
+
datatype: v.node.datatype.clone(),
|
|
1264
|
+
nodegroup_id: v.node.nodegroup_id.clone(),
|
|
1265
|
+
tile_id: v.tile.as_ref().and_then(|t| t.tileid.clone()),
|
|
1266
|
+
tile_data: v.tile_data.clone(),
|
|
1267
|
+
is_collector: v.is_collector,
|
|
1268
|
+
independent: v.independent,
|
|
1269
|
+
child_node_ids: v.child_node_ids.clone(),
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
#[derive(serde::Serialize)]
|
|
1275
|
+
struct NapiPseudoListJson {
|
|
1276
|
+
node_alias: String,
|
|
1277
|
+
is_single: bool,
|
|
1278
|
+
is_loaded: bool,
|
|
1279
|
+
values: Vec<NapiPseudoValueJson>,
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
impl From<&PseudoListCore> for NapiPseudoListJson {
|
|
1283
|
+
fn from(l: &PseudoListCore) -> Self {
|
|
1284
|
+
NapiPseudoListJson {
|
|
1285
|
+
node_alias: l.node_alias.clone(),
|
|
1286
|
+
is_single: l.is_single,
|
|
1287
|
+
is_loaded: l.is_loaded,
|
|
1288
|
+
values: l.values.iter().map(NapiPseudoValueJson::from).collect(),
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// =============================================================================
|
|
1294
|
+
// NapiResourceModelWrapper
|
|
1295
|
+
// =============================================================================
|
|
1296
|
+
|
|
1297
|
+
/// NAPI equivalent of WASMResourceModelWrapper.
|
|
1298
|
+
///
|
|
1299
|
+
/// Provides graph schema access (nodes, edges, nodegroups, permissions, pruning)
|
|
1300
|
+
/// so that the TS ResourceModelWrapper can delegate to either this or the WASM
|
|
1301
|
+
/// model wrapper via the backend abstraction.
|
|
1302
|
+
#[napi]
|
|
1303
|
+
pub struct NapiResourceModelWrapper {
|
|
1304
|
+
model_access: GraphModelAccess,
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
#[napi]
|
|
1308
|
+
impl NapiResourceModelWrapper {
|
|
1309
|
+
/// Create a model wrapper from a graph JSON string.
|
|
1310
|
+
///
|
|
1311
|
+
/// The graph is also registered in the core GRAPH_REGISTRY so that
|
|
1312
|
+
/// NapiResourceInstanceWrapper can look it up by graph_id.
|
|
1313
|
+
#[napi(constructor)]
|
|
1314
|
+
pub fn new(graph_json: String, default_allow: bool) -> Result<Self> {
|
|
1315
|
+
let mut graph: StaticGraph = serde_json::from_str(&graph_json)
|
|
1316
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid graph JSON: {}", e)))?;
|
|
1317
|
+
|
|
1318
|
+
graph.build_indices();
|
|
1319
|
+
let graph_id = graph.graphid.clone();
|
|
1320
|
+
let graph_arc = Arc::new(graph);
|
|
1321
|
+
|
|
1322
|
+
// Register in core graph registry
|
|
1323
|
+
alizarin_core::register_graph(&graph_id, Arc::clone(&graph_arc));
|
|
1324
|
+
|
|
1325
|
+
let model_access = GraphModelAccess::new_eager(graph_arc, default_allow);
|
|
1326
|
+
|
|
1327
|
+
Ok(NapiResourceModelWrapper { model_access })
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/// Create from an already-loaded NapiStaticGraph.
|
|
1331
|
+
#[napi(factory)]
|
|
1332
|
+
pub fn from_graph(graph: &crate::NapiStaticGraph, default_allow: bool) -> Result<Self> {
|
|
1333
|
+
let inner = graph.inner_ref();
|
|
1334
|
+
let graph_id = inner.graphid.clone();
|
|
1335
|
+
let mut cloned = inner.clone();
|
|
1336
|
+
cloned.build_indices();
|
|
1337
|
+
let graph_arc = Arc::new(cloned);
|
|
1338
|
+
|
|
1339
|
+
alizarin_core::register_graph(&graph_id, Arc::clone(&graph_arc));
|
|
1340
|
+
|
|
1341
|
+
let model_access = GraphModelAccess::new_eager(graph_arc, default_allow);
|
|
1342
|
+
|
|
1343
|
+
Ok(NapiResourceModelWrapper { model_access })
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// =========================================================================
|
|
1347
|
+
// Graph ID
|
|
1348
|
+
// =========================================================================
|
|
1349
|
+
|
|
1350
|
+
#[napi(js_name = "getGraphId")]
|
|
1351
|
+
pub fn get_graph_id(&self) -> String {
|
|
1352
|
+
self.model_access.get_graph().graphid.clone()
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// =========================================================================
|
|
1356
|
+
// Graph getter/setter
|
|
1357
|
+
// =========================================================================
|
|
1358
|
+
|
|
1359
|
+
#[napi(getter)]
|
|
1360
|
+
pub fn graph(&self) -> Result<serde_json::Value> {
|
|
1361
|
+
serde_json::to_value(self.model_access.get_graph())
|
|
1362
|
+
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
#[napi(setter)]
|
|
1366
|
+
pub fn set_graph(&mut self, graph_json: String) -> Result<()> {
|
|
1367
|
+
let graph: StaticGraph = serde_json::from_str(&graph_json)
|
|
1368
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid graph JSON: {}", e)))?;
|
|
1369
|
+
self.model_access.rebuild_from_graph(&graph);
|
|
1370
|
+
Ok(())
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// =========================================================================
|
|
1374
|
+
// Build nodes (no-op — caches are built eagerly in constructor)
|
|
1375
|
+
// =========================================================================
|
|
1376
|
+
|
|
1377
|
+
#[napi(js_name = "buildNodes")]
|
|
1378
|
+
pub fn build_nodes(&self) -> Result<()> {
|
|
1379
|
+
// GraphModelAccess builds caches in from_graph(), so this is a no-op.
|
|
1380
|
+
Ok(())
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
#[napi(js_name = "buildNodesForGraph")]
|
|
1384
|
+
pub fn build_nodes_for_graph(&mut self, graph_json: String) -> Result<()> {
|
|
1385
|
+
let graph: StaticGraph = serde_json::from_str(&graph_json)
|
|
1386
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid graph JSON: {}", e)))?;
|
|
1387
|
+
self.model_access.rebuild_from_graph(&graph);
|
|
1388
|
+
Ok(())
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// =========================================================================
|
|
1392
|
+
// Node accessors
|
|
1393
|
+
// =========================================================================
|
|
1394
|
+
|
|
1395
|
+
/// Get the root node of the graph.
|
|
1396
|
+
#[napi(js_name = "getRootNode")]
|
|
1397
|
+
pub fn get_root_node(&self) -> Result<serde_json::Value> {
|
|
1398
|
+
let root = self
|
|
1399
|
+
.model_access
|
|
1400
|
+
.get_root_node()
|
|
1401
|
+
.map_err(napi::Error::from_reason)?;
|
|
1402
|
+
serde_json::to_value(&*root).map_err(|e| napi::Error::from_reason(e.to_string()))
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/// Get all nodes as a Map<string, StaticNode>.
|
|
1406
|
+
#[napi(js_name = "getNodeObjects")]
|
|
1407
|
+
pub fn get_node_objects(&self) -> Result<HashMap<String, serde_json::Value>> {
|
|
1408
|
+
let nodes = self
|
|
1409
|
+
.model_access
|
|
1410
|
+
.get_nodes()
|
|
1411
|
+
.ok_or_else(|| napi::Error::from_reason("Nodes not available"))?;
|
|
1412
|
+
let mut result = HashMap::new();
|
|
1413
|
+
for (k, v) in nodes {
|
|
1414
|
+
result.insert(
|
|
1415
|
+
k.clone(),
|
|
1416
|
+
serde_json::to_value(&**v).map_err(|e| napi::Error::from_reason(e.to_string()))?,
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
Ok(result)
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/// Get all nodes indexed by alias.
|
|
1423
|
+
#[napi(js_name = "getNodeObjectsByAlias")]
|
|
1424
|
+
pub fn get_node_objects_by_alias(&self) -> Result<HashMap<String, serde_json::Value>> {
|
|
1425
|
+
let nodes = self
|
|
1426
|
+
.model_access
|
|
1427
|
+
.get_nodes_by_alias()
|
|
1428
|
+
.ok_or_else(|| napi::Error::from_reason("Nodes by alias not available"))?;
|
|
1429
|
+
let mut result = HashMap::new();
|
|
1430
|
+
for (k, v) in nodes {
|
|
1431
|
+
result.insert(
|
|
1432
|
+
k.clone(),
|
|
1433
|
+
serde_json::to_value(&**v).map_err(|e| napi::Error::from_reason(e.to_string()))?,
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
Ok(result)
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/// Get a single node by alias.
|
|
1440
|
+
#[napi(js_name = "getNodeObjectFromAlias")]
|
|
1441
|
+
pub fn get_node_object_from_alias(&self, alias: String) -> Result<serde_json::Value> {
|
|
1442
|
+
let nodes = self
|
|
1443
|
+
.model_access
|
|
1444
|
+
.get_nodes_by_alias()
|
|
1445
|
+
.ok_or_else(|| napi::Error::from_reason("Nodes by alias not available"))?;
|
|
1446
|
+
let node = nodes.get(&alias).ok_or_else(|| {
|
|
1447
|
+
napi::Error::from_reason(format!("Node not found for alias: {}", alias))
|
|
1448
|
+
})?;
|
|
1449
|
+
serde_json::to_value(&**node).map_err(|e| napi::Error::from_reason(e.to_string()))
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/// Get a single node by node ID.
|
|
1453
|
+
#[napi(js_name = "getNodeObjectFromId")]
|
|
1454
|
+
pub fn get_node_object_from_id(&self, id: String) -> Result<serde_json::Value> {
|
|
1455
|
+
let nodes = self
|
|
1456
|
+
.model_access
|
|
1457
|
+
.get_nodes()
|
|
1458
|
+
.ok_or_else(|| napi::Error::from_reason("Nodes not available"))?;
|
|
1459
|
+
let node = nodes
|
|
1460
|
+
.get(&id)
|
|
1461
|
+
.ok_or_else(|| napi::Error::from_reason(format!("Node not found: {}", id)))?;
|
|
1462
|
+
serde_json::to_value(&**node).map_err(|e| napi::Error::from_reason(e.to_string()))
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/// Get child nodes for a parent node ID.
|
|
1466
|
+
#[napi(js_name = "getChildNodes")]
|
|
1467
|
+
pub fn get_child_nodes(&self, node_id: String) -> Result<HashMap<String, serde_json::Value>> {
|
|
1468
|
+
let children = self.model_access.get_child_nodes(&node_id);
|
|
1469
|
+
let mut result = HashMap::new();
|
|
1470
|
+
for (alias, node) in &children {
|
|
1471
|
+
result.insert(
|
|
1472
|
+
alias.clone(),
|
|
1473
|
+
serde_json::to_value(&**node)
|
|
1474
|
+
.map_err(|e| napi::Error::from_reason(e.to_string()))?,
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
Ok(result)
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/// Get child node aliases (just strings, not full objects).
|
|
1481
|
+
#[napi(js_name = "getChildNodeAliases")]
|
|
1482
|
+
pub fn get_child_node_aliases(&self, node_id: String) -> Vec<String> {
|
|
1483
|
+
self.model_access
|
|
1484
|
+
.get_child_nodes(&node_id)
|
|
1485
|
+
.keys()
|
|
1486
|
+
.cloned()
|
|
1487
|
+
.collect()
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// =========================================================================
|
|
1491
|
+
// Edge accessors
|
|
1492
|
+
// =========================================================================
|
|
1493
|
+
|
|
1494
|
+
/// Get edges as Map<parent_id, [child_ids]>.
|
|
1495
|
+
#[napi(js_name = "getEdges")]
|
|
1496
|
+
pub fn get_edges(&self) -> Result<HashMap<String, Vec<String>>> {
|
|
1497
|
+
let edges = self
|
|
1498
|
+
.model_access
|
|
1499
|
+
.get_edges()
|
|
1500
|
+
.ok_or_else(|| napi::Error::from_reason("Edges not available"))?;
|
|
1501
|
+
Ok(edges.clone())
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// =========================================================================
|
|
1505
|
+
// Nodegroup accessors
|
|
1506
|
+
// =========================================================================
|
|
1507
|
+
|
|
1508
|
+
/// Get all nodegroups.
|
|
1509
|
+
#[napi(js_name = "getNodegroupObjects")]
|
|
1510
|
+
pub fn get_nodegroup_objects(&self) -> Result<HashMap<String, serde_json::Value>> {
|
|
1511
|
+
let nodegroups = self
|
|
1512
|
+
.model_access
|
|
1513
|
+
.get_nodegroups()
|
|
1514
|
+
.ok_or_else(|| napi::Error::from_reason("Nodegroups not available"))?;
|
|
1515
|
+
let mut result = HashMap::new();
|
|
1516
|
+
for (k, v) in nodegroups {
|
|
1517
|
+
result.insert(
|
|
1518
|
+
k.clone(),
|
|
1519
|
+
serde_json::to_value(&**v).map_err(|e| napi::Error::from_reason(e.to_string()))?,
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
Ok(result)
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/// Get nodegroup IDs.
|
|
1526
|
+
#[napi(js_name = "getNodegroupIds")]
|
|
1527
|
+
pub fn get_nodegroup_ids(&self) -> Vec<String> {
|
|
1528
|
+
self.model_access
|
|
1529
|
+
.get_nodegroups()
|
|
1530
|
+
.map(|ng| ng.keys().cloned().collect())
|
|
1531
|
+
.unwrap_or_default()
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/// Get nodegroup name (actually the name of the root node for that nodegroup).
|
|
1535
|
+
#[napi(js_name = "getNodegroupName")]
|
|
1536
|
+
pub fn get_nodegroup_name(&self, nodegroup_id: String) -> Result<String> {
|
|
1537
|
+
let nodes = self
|
|
1538
|
+
.model_access
|
|
1539
|
+
.get_nodes()
|
|
1540
|
+
.ok_or_else(|| napi::Error::from_reason("Nodes not available"))?;
|
|
1541
|
+
let node = nodes
|
|
1542
|
+
.get(&nodegroup_id)
|
|
1543
|
+
.ok_or_else(|| napi::Error::from_reason(format!("Node not found: {}", nodegroup_id)))?;
|
|
1544
|
+
Ok(node.name.clone())
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/// Get node ID from alias.
|
|
1548
|
+
#[napi(js_name = "getNodeIdFromAlias")]
|
|
1549
|
+
pub fn get_node_id_from_alias(&self, alias: String) -> Result<String> {
|
|
1550
|
+
let nodes = self
|
|
1551
|
+
.model_access
|
|
1552
|
+
.get_nodes_by_alias()
|
|
1553
|
+
.ok_or_else(|| napi::Error::from_reason("Nodes by alias not available"))?;
|
|
1554
|
+
let node = nodes.get(&alias).ok_or_else(|| {
|
|
1555
|
+
napi::Error::from_reason(format!("Node not found for alias: {}", alias))
|
|
1556
|
+
})?;
|
|
1557
|
+
Ok(node.nodeid.clone())
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// =========================================================================
|
|
1561
|
+
// Property getters (matching WASM's getter properties)
|
|
1562
|
+
// =========================================================================
|
|
1563
|
+
|
|
1564
|
+
#[napi(getter)]
|
|
1565
|
+
pub fn nodes(&self) -> Result<HashMap<String, serde_json::Value>> {
|
|
1566
|
+
self.get_node_objects()
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
#[napi(getter, js_name = "nodesByAlias")]
|
|
1570
|
+
pub fn nodes_by_alias(&self) -> Result<HashMap<String, serde_json::Value>> {
|
|
1571
|
+
self.get_node_objects_by_alias()
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
#[napi(getter)]
|
|
1575
|
+
pub fn edges(&self) -> Result<HashMap<String, Vec<String>>> {
|
|
1576
|
+
self.get_edges()
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
#[napi(getter)]
|
|
1580
|
+
pub fn nodegroups(&self) -> Result<HashMap<String, serde_json::Value>> {
|
|
1581
|
+
self.get_nodegroup_objects()
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// =========================================================================
|
|
1585
|
+
// Permission management
|
|
1586
|
+
// =========================================================================
|
|
1587
|
+
|
|
1588
|
+
/// Get permitted nodegroups as boolean map.
|
|
1589
|
+
#[napi(js_name = "getPermittedNodegroups")]
|
|
1590
|
+
pub fn get_permitted_nodegroups(&self) -> HashMap<String, bool> {
|
|
1591
|
+
self.model_access.get_permitted_nodegroups_bool()
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/// Check if a nodegroup is permitted.
|
|
1595
|
+
#[napi(js_name = "isNodegroupPermitted")]
|
|
1596
|
+
pub fn is_nodegroup_permitted(&self, nodegroup_id: String) -> bool {
|
|
1597
|
+
self.model_access.is_nodegroup_permitted(&nodegroup_id)
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/// Set permitted nodegroups. Accepts an object with boolean values or
|
|
1601
|
+
/// conditional rule objects { path, allowed }.
|
|
1602
|
+
#[napi(js_name = "setPermittedNodegroups")]
|
|
1603
|
+
pub fn set_permitted_nodegroups(&mut self, permissions: serde_json::Value) -> Result<()> {
|
|
1604
|
+
let perms = Self::parse_permission_value(&permissions)?;
|
|
1605
|
+
self.model_access.set_permitted_nodegroups_rules(perms);
|
|
1606
|
+
Ok(())
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
#[napi(js_name = "setDefaultAllowAllNodegroups")]
|
|
1610
|
+
pub fn set_default_allow_all_nodegroups(&mut self, default_allow: bool) {
|
|
1611
|
+
self.model_access.set_default_allow(default_allow);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// =========================================================================
|
|
1615
|
+
// Graph modification
|
|
1616
|
+
// =========================================================================
|
|
1617
|
+
|
|
1618
|
+
#[napi(js_name = "setGraphNodes")]
|
|
1619
|
+
pub fn set_graph_nodes(&mut self, nodes_json: String) -> Result<()> {
|
|
1620
|
+
let nodes: Vec<StaticNode> = serde_json::from_str(&nodes_json)
|
|
1621
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid nodes JSON: {}", e)))?;
|
|
1622
|
+
self.model_access.set_graph_nodes(nodes);
|
|
1623
|
+
Ok(())
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
#[napi(js_name = "setGraphEdges")]
|
|
1627
|
+
pub fn set_graph_edges(&mut self, edges_json: String) -> Result<()> {
|
|
1628
|
+
let edges: Vec<StaticEdge> = serde_json::from_str(&edges_json)
|
|
1629
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid edges JSON: {}", e)))?;
|
|
1630
|
+
self.model_access.set_graph_edges(edges);
|
|
1631
|
+
Ok(())
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
#[napi(js_name = "setGraphNodegroups")]
|
|
1635
|
+
pub fn set_graph_nodegroups(&mut self, nodegroups_json: String) -> Result<()> {
|
|
1636
|
+
let nodegroups: Vec<StaticNodegroup> = serde_json::from_str(&nodegroups_json)
|
|
1637
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid nodegroups JSON: {}", e)))?;
|
|
1638
|
+
self.model_access.set_graph_nodegroups(nodegroups);
|
|
1639
|
+
Ok(())
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// =========================================================================
|
|
1643
|
+
// Graph pruning
|
|
1644
|
+
// =========================================================================
|
|
1645
|
+
|
|
1646
|
+
/// Prune graph to only include permitted nodegroups and their dependencies.
|
|
1647
|
+
#[napi(js_name = "pruneGraph")]
|
|
1648
|
+
pub fn prune_graph(&mut self, keep_functions: Option<Vec<String>>) -> Result<()> {
|
|
1649
|
+
let keep_fns_ref = keep_functions.as_deref();
|
|
1650
|
+
self.model_access
|
|
1651
|
+
.prune_graph(keep_fns_ref)
|
|
1652
|
+
.map_err(napi::Error::from_reason)
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Private helpers for NapiResourceModelWrapper
|
|
1657
|
+
impl NapiResourceModelWrapper {
|
|
1658
|
+
/// Parse permission values from a JSON object.
|
|
1659
|
+
/// Supports booleans and conditional rules: { path: "...", allowed: [...] }
|
|
1660
|
+
fn parse_permission_value(val: &serde_json::Value) -> Result<HashMap<String, PermissionRule>> {
|
|
1661
|
+
let obj = val
|
|
1662
|
+
.as_object()
|
|
1663
|
+
.ok_or_else(|| napi::Error::from_reason("Permissions must be an object"))?;
|
|
1664
|
+
let mut perms = HashMap::new();
|
|
1665
|
+
for (key, value) in obj {
|
|
1666
|
+
if let Some(b) = value.as_bool() {
|
|
1667
|
+
perms.insert(key.clone(), PermissionRule::Boolean(b));
|
|
1668
|
+
} else if let Some(obj) = value.as_object() {
|
|
1669
|
+
let path = obj
|
|
1670
|
+
.get("path")
|
|
1671
|
+
.and_then(|v| v.as_str())
|
|
1672
|
+
.map(String::from)
|
|
1673
|
+
.unwrap_or_default();
|
|
1674
|
+
let allowed = obj
|
|
1675
|
+
.get("allowed")
|
|
1676
|
+
.and_then(|v| v.as_array())
|
|
1677
|
+
.map(|arr| {
|
|
1678
|
+
arr.iter()
|
|
1679
|
+
.filter_map(|v| v.as_str().map(String::from))
|
|
1680
|
+
.collect::<HashSet<String>>()
|
|
1681
|
+
})
|
|
1682
|
+
.unwrap_or_default();
|
|
1683
|
+
let rule = PermissionRule::conditional(path, allowed).map_err(|e| {
|
|
1684
|
+
napi::Error::from_reason(format!(
|
|
1685
|
+
"Invalid conditional rule for '{}': {}",
|
|
1686
|
+
key, e
|
|
1687
|
+
))
|
|
1688
|
+
})?;
|
|
1689
|
+
perms.insert(key.clone(), rule);
|
|
1690
|
+
} else {
|
|
1691
|
+
return Err(napi::Error::from_reason(format!(
|
|
1692
|
+
"Invalid permission value for '{}': expected boolean or {{path, allowed}}",
|
|
1693
|
+
key
|
|
1694
|
+
)));
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
Ok(perms)
|
|
1698
|
+
}
|
|
1699
|
+
}
|