@alizarin/napi 2.0.0-alpha.91 → 2.0.0-alpha.94

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 CHANGED
@@ -16,6 +16,7 @@ napi = { version = "2", features = ["napi6", "serde-json"] }
16
16
  napi-derive = "2"
17
17
  serde = { version = "1.0", features = ["derive"] }
18
18
  serde_json = "1.0"
19
+ uuid = { version = "1", features = ["v4", "v5"] }
19
20
 
20
21
  [build-dependencies]
21
22
  napi-build = "2"
@@ -345,7 +345,7 @@ describe('NapiPseudoValue property names', () => {
345
345
 
346
346
  // Create instance wrapper and load tiles
347
347
  const instanceWrapper = new NapiResourceInstanceWrapper(resource.resourceinstance.graph_id);
348
- instanceWrapper.loadTilesFromResource(resource);
348
+ instanceWrapper.loadTilesFromResource(JSON.stringify(resource));
349
349
 
350
350
  // Get a PseudoList and PseudoValue
351
351
  const pseudoList = instanceWrapper.getValuesAtPath('basic_info.name');
@@ -433,13 +433,13 @@ describe('NapiResourceInstanceWrapper methods', () => {
433
433
 
434
434
  it('has setTileDataForNode method', () => {
435
435
  const wrapper = new NapiResourceInstanceWrapper(graphId);
436
- wrapper.loadTilesFromResource(resource);
436
+ wrapper.loadTilesFromResource(JSON.stringify(resource));
437
437
  assert.equal(typeof wrapper.setTileDataForNode, 'function');
438
438
  });
439
439
 
440
440
  it('setTileDataForNode mutates tile data', () => {
441
441
  const wrapper = new NapiResourceInstanceWrapper(graphId);
442
- wrapper.loadTilesFromResource(resource);
442
+ wrapper.loadTilesFromResource(JSON.stringify(resource));
443
443
 
444
444
  const tileIds = wrapper.getAllTileIds();
445
445
  assert.ok(tileIds.length > 0, 'should have tiles');
@@ -459,13 +459,13 @@ describe('NapiResourceInstanceWrapper methods', () => {
459
459
  assert.equal(typeof wrapper.tilesLoaded, 'function');
460
460
  assert.equal(wrapper.tilesLoaded(), false);
461
461
 
462
- wrapper.loadTilesFromResource(resource);
462
+ wrapper.loadTilesFromResource(JSON.stringify(resource));
463
463
  assert.equal(wrapper.tilesLoaded(), true);
464
464
  });
465
465
 
466
466
  it('has toJson method', () => {
467
467
  const wrapper = new NapiResourceInstanceWrapper(graphId);
468
- wrapper.loadTilesFromResource(resource);
468
+ wrapper.loadTilesFromResource(JSON.stringify(resource));
469
469
  assert.equal(typeof wrapper.toJson, 'function');
470
470
  // toJson requires populate() first; just verify the method exists
471
471
  // and that calling it without populate gives a meaningful error
@@ -481,7 +481,7 @@ describe('NapiResourceInstanceWrapper methods', () => {
481
481
 
482
482
  it('exportTilesJson returns valid JSON array of tiles', () => {
483
483
  const wrapper = new NapiResourceInstanceWrapper(graphId);
484
- wrapper.loadTilesFromResource(resource);
484
+ wrapper.loadTilesFromResource(JSON.stringify(resource));
485
485
 
486
486
  const json = wrapper.exportTilesJson();
487
487
  assert.equal(typeof json, 'string');
@@ -499,7 +499,7 @@ describe('NapiResourceInstanceWrapper methods', () => {
499
499
 
500
500
  it('exportTilesJson reflects setTileDataForNode mutations', () => {
501
501
  const wrapper = new NapiResourceInstanceWrapper(graphId);
502
- wrapper.loadTilesFromResource(resource);
502
+ wrapper.loadTilesFromResource(JSON.stringify(resource));
503
503
 
504
504
  const tileIds = wrapper.getAllTileIds();
505
505
  const tileId = tileIds[0];
Binary file
Binary file
Binary file
Binary file
package/index.d.ts CHANGED
@@ -40,6 +40,28 @@ export declare function extensionResolveMarkers(datatype: string, tileData: any,
40
40
  export declare function hasExtensionHandler(datatype: string): boolean
41
41
  /** List all registered extension handler datatypes. */
42
42
  export declare function getRegisteredExtensionHandlers(): Array<string>
43
+ /** Parse SKOS RDF/XML and return all collections as a JSON array. */
44
+ export declare function parseSkosXml(xmlContent: string, baseUri: string): any
45
+ /** Parse SKOS RDF/XML and return a single collection (the first one found). */
46
+ export declare function parseSkosXmlToCollection(xmlContent: string, baseUri: string): any
47
+ /**
48
+ * Build a mapping from node alias to collection ID based on graph definition.
49
+ *
50
+ * Equivalent to WASM's `buildAliasToCollectionMap`.
51
+ */
52
+ export declare function buildAliasToCollectionMap(graphJson: string, resolvableDatatypes?: Array<string> | undefined | null, configKeys?: Array<string> | undefined | null): Record<string, string>
53
+ /**
54
+ * Scan a JSON tree to find which collections are needed for resolution.
55
+ *
56
+ * Equivalent to WASM's `findNeededCollections`.
57
+ */
58
+ export declare function findNeededCollections(treeJson: string, aliasToCollection: Record<string, string>): Array<string>
59
+ /** Check if a string is a valid UUID. */
60
+ export declare function isValidUuid(s: string): boolean
61
+ /** Get the default resolvable datatypes (concept, concept-list). */
62
+ export declare function getDefaultResolvableDatatypes(): Array<string>
63
+ /** Get the default config keys for collection IDs. */
64
+ export declare function getDefaultConfigKeys(): Array<string>
43
65
  /**
44
66
  * Import a prebuild/pkg directory: register graphs, load SKOS collections,
45
67
  * and load ontology configs.
@@ -58,6 +80,18 @@ export declare class NapiRdmCache {
58
80
  /** Load a single collection from JSON. */
59
81
  loadCollectionJson(jsonStr: string): void
60
82
  get collectionCount(): number
83
+ /** Check if a collection is already in the cache. */
84
+ hasCollection(collectionId: string): boolean
85
+ /** Add a collection from a JSON string of concepts. */
86
+ addCollectionFromJson(collectionId: string, jsonStr: string): void
87
+ /** Get the parent concept ID for a concept within a collection. */
88
+ getParentId(collectionId: string, conceptId: string): string | null
89
+ /** Remove a collection from the cache. */
90
+ removeCollection(collectionId: string): boolean
91
+ /** Clear all collections from the cache. */
92
+ clear(): void
93
+ /** Resolve labels to UUIDs using this cache for lookups. */
94
+ resolveLabels(treeJson: string, aliasToCollection: Record<string, string>, strict: boolean): any
61
95
  }
62
96
  export declare class NapiNodeConfigManager {
63
97
  constructor()
@@ -66,6 +100,33 @@ export declare class NapiNodeConfigManager {
66
100
  /** Build node configs from a NapiStaticGraph. */
67
101
  buildFromGraph(graph: NapiStaticGraph): void
68
102
  }
103
+ export declare class NapiTileData {
104
+ has(key: string): boolean
105
+ get(key: string): any
106
+ set(key: string, value: any): void
107
+ delete(key: string): boolean
108
+ keys(): Array<string>
109
+ }
110
+ export declare class NapiStaticTile {
111
+ constructor(nodegroupId: string, tileid?: string | undefined | null, sortorder?: number | undefined | null, resourceinstanceId?: string | undefined | null, parenttileId?: string | undefined | null)
112
+ get data(): NapiTileData
113
+ /**
114
+ * Setter is a no-op — data is always accessed via the NapiTileData getter.
115
+ * Exists to prevent TypeError in strict mode when JS does
116
+ * `tile.data = tile.data || new Map()`.
117
+ */
118
+ set data(value: any)
119
+ get tileid(): string | null
120
+ set tileid(value?: string | undefined | null)
121
+ get nodegroupId(): string
122
+ get sortorder(): number | null
123
+ get resourceinstanceId(): string
124
+ get parenttileId(): string | null
125
+ set parenttileId(value?: string | undefined | null)
126
+ get provisionaledits(): any
127
+ /** Generate a tile ID if not already set. */
128
+ ensureId(): string
129
+ }
69
130
  export declare class NapiPseudoValue {
70
131
  get node(): any
71
132
  get nodeId(): string | null
@@ -96,6 +157,12 @@ export declare class NapiPseudoValue {
96
157
  toSnapshot(): any
97
158
  /** Clear tile data */
98
159
  clear(): void
160
+ get tile(): NapiStaticTile | null
161
+ set tile(tile?: NapiStaticTile | undefined | null)
162
+ /** Get the inner PseudoValueCore wrapped as NapiPseudoValue. */
163
+ get inner(): NapiPseudoValue | null
164
+ /** Returns null — value is managed JS-side via _cachedValue. */
165
+ get value(): any
99
166
  }
100
167
  export declare class NapiPseudoList {
101
168
  get nodeAlias(): string
@@ -128,10 +195,15 @@ export declare class NapiValuesFromNodegroupResult {
128
195
  export declare class NapiResourceInstanceWrapper {
129
196
  /** Create a wrapper for a given graph (must be registered). */
130
197
  constructor(graphId: string)
131
- /** Load tiles from a JSON array. */
132
- loadTiles(tilesJs: any): void
133
- /** Load tiles directly from a StaticResource JSON. */
134
- loadTilesFromResource(resourceJs: any): void
198
+ /** Load tiles from a JSON string (single-pass deserialization). */
199
+ loadTiles(tilesJson: string): void
200
+ /** Load tiles directly from a StaticResource JSON string (single-pass deserialization). */
201
+ loadTilesFromResource(resourceJson: string): void
202
+ /**
203
+ * Load tiles directly from a NapiStaticResourceRegistry by resource ID.
204
+ * This avoids the JS round-trip of serializing tiles to JS and back.
205
+ */
206
+ loadFromRegistry(resourceId: string, registry: NapiStaticResourceRegistry): boolean
135
207
  /** Append tiles incrementally (for lazy loading). */
136
208
  appendTiles(tilesJs: any): void
137
209
  getTileCount(): number
@@ -169,6 +241,13 @@ export declare class NapiResourceInstanceWrapper {
169
241
  ensureNodegroup(allValuesJs: any, allNodegroupsJs: any, nodegroupId: string, addIfMissing: boolean, nodegroupPermissionsJs: any, doImpliedNodegroups: boolean): NapiEnsureNodegroupResult
170
242
  /** Build pseudo values from tiles for a specific nodegroup. */
171
243
  valuesFromResourceNodegroup(existingValuesJs: any, nodegroupTileIds: Array<string>, nodegroupId: string): NapiValuesFromNodegroupResult
244
+ /**
245
+ * Resolve a dot-separated path to its target node metadata without needing tiles.
246
+ *
247
+ * Returns { nodegroupId, isSingle, targetNodeId } — enough for the JS layer to
248
+ * lazy-load just that nodegroup's tiles before calling getValuesAtPath.
249
+ */
250
+ resolvePath(path: string): any
172
251
  /** Resolve a dot-separated path and return a PseudoList. */
173
252
  getValuesAtPath(path: string): NapiPseudoList
174
253
  /**
package/index.js CHANGED
@@ -310,10 +310,12 @@ if (!nativeBinding) {
310
310
  throw new Error(`Failed to load native binding`)
311
311
  }
312
312
 
313
- const { NapiRdmCache, NapiNodeConfigManager, NapiPseudoValue, NapiPseudoList, NapiPopulateResult, NapiEnsureNodegroupResult, NapiValuesFromNodegroupResult, NapiResourceInstanceWrapper, NapiResourceModelWrapper, NapiPrebuildExporter, NapiStaticGraph, NapiStaticResourceRegistry, buildGraphFromCsvs, buildBusinessDataFromCsv, extensionCoerce, extensionRenderDisplay, extensionResolveMarkers, hasExtensionHandler, getRegisteredExtensionHandlers, importPrebuild } = nativeBinding
313
+ const { NapiRdmCache, NapiNodeConfigManager, NapiTileData, NapiStaticTile, NapiPseudoValue, NapiPseudoList, NapiPopulateResult, NapiEnsureNodegroupResult, NapiValuesFromNodegroupResult, NapiResourceInstanceWrapper, NapiResourceModelWrapper, NapiPrebuildExporter, NapiStaticGraph, NapiStaticResourceRegistry, buildGraphFromCsvs, buildBusinessDataFromCsv, extensionCoerce, extensionRenderDisplay, extensionResolveMarkers, hasExtensionHandler, getRegisteredExtensionHandlers, parseSkosXml, parseSkosXmlToCollection, buildAliasToCollectionMap, findNeededCollections, isValidUuid, getDefaultResolvableDatatypes, getDefaultConfigKeys, importPrebuild } = nativeBinding
314
314
 
315
315
  module.exports.NapiRdmCache = NapiRdmCache
316
316
  module.exports.NapiNodeConfigManager = NapiNodeConfigManager
317
+ module.exports.NapiTileData = NapiTileData
318
+ module.exports.NapiStaticTile = NapiStaticTile
317
319
  module.exports.NapiPseudoValue = NapiPseudoValue
318
320
  module.exports.NapiPseudoList = NapiPseudoList
319
321
  module.exports.NapiPopulateResult = NapiPopulateResult
@@ -331,4 +333,11 @@ module.exports.extensionRenderDisplay = extensionRenderDisplay
331
333
  module.exports.extensionResolveMarkers = extensionResolveMarkers
332
334
  module.exports.hasExtensionHandler = hasExtensionHandler
333
335
  module.exports.getRegisteredExtensionHandlers = getRegisteredExtensionHandlers
336
+ module.exports.parseSkosXml = parseSkosXml
337
+ module.exports.parseSkosXmlToCollection = parseSkosXmlToCollection
338
+ module.exports.buildAliasToCollectionMap = buildAliasToCollectionMap
339
+ module.exports.findNeededCollections = findNeededCollections
340
+ module.exports.isValidUuid = isValidUuid
341
+ module.exports.getDefaultResolvableDatatypes = getDefaultResolvableDatatypes
342
+ module.exports.getDefaultConfigKeys = getDefaultConfigKeys
334
343
  module.exports.importPrebuild = importPrebuild
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alizarin/napi",
3
- "version": "2.0.0-alpha.91",
3
+ "version": "2.0.0-alpha.94",
4
4
  "license": "AGPL-3.0-or-later",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,8 +30,8 @@
30
30
  "@napi-rs/cli": "^2.18.0"
31
31
  },
32
32
  "optionalDependencies": {
33
- "@alizarin/napi-win32-x64-msvc": "2.0.0-alpha.91",
34
- "@alizarin/napi-darwin-x64": "2.0.0-alpha.91",
35
- "@alizarin/napi-linux-x64-gnu": "2.0.0-alpha.91"
33
+ "@alizarin/napi-win32-x64-msvc": "2.0.0-alpha.94",
34
+ "@alizarin/napi-darwin-x64": "2.0.0-alpha.94",
35
+ "@alizarin/napi-linux-x64-gnu": "2.0.0-alpha.94"
36
36
  }
37
37
  }
@@ -4,7 +4,7 @@
4
4
  /// the TypeScript ViewModel layer (ResourceInstanceWrapper / PseudoValue / PseudoList)
5
5
  /// can work with either backend.
6
6
  use std::collections::HashMap;
7
- use std::sync::Arc;
7
+ use std::sync::{Arc, Mutex};
8
8
 
9
9
  use napi::bindgen_prelude::*;
10
10
  use napi_derive::napi;
@@ -82,6 +82,64 @@ impl NapiRdmCache {
82
82
  pub fn collection_count(&self) -> u32 {
83
83
  self.inner.len() as u32
84
84
  }
85
+
86
+ /// Check if a collection is already in the cache.
87
+ #[napi]
88
+ pub fn has_collection(&self, collection_id: String) -> bool {
89
+ self.inner.has_collection(&collection_id)
90
+ }
91
+
92
+ /// Add a collection from a JSON string of concepts.
93
+ #[napi]
94
+ pub fn add_collection_from_json(
95
+ &mut self,
96
+ collection_id: String,
97
+ json_str: String,
98
+ ) -> Result<()> {
99
+ self.inner
100
+ .add_collection_from_json(&collection_id, &json_str)
101
+ .map_err(|e| napi::Error::from_reason(format!("Failed to add collection: {e}")))
102
+ }
103
+
104
+ /// Get the parent concept ID for a concept within a collection.
105
+ #[napi]
106
+ pub fn get_parent_id(&self, collection_id: String, concept_id: String) -> Option<String> {
107
+ self.inner.get_parent_id(&collection_id, &concept_id)
108
+ }
109
+
110
+ /// Remove a collection from the cache.
111
+ #[napi]
112
+ pub fn remove_collection(&mut self, collection_id: String) -> bool {
113
+ self.inner.remove_collection(&collection_id)
114
+ }
115
+
116
+ /// Clear all collections from the cache.
117
+ #[napi]
118
+ pub fn clear(&mut self) {
119
+ self.inner.clear();
120
+ }
121
+
122
+ /// Resolve labels to UUIDs using this cache for lookups.
123
+ #[napi]
124
+ pub fn resolve_labels(
125
+ &self,
126
+ tree_json: String,
127
+ alias_to_collection: HashMap<String, String>,
128
+ strict: bool,
129
+ ) -> Result<serde_json::Value> {
130
+ let tree: serde_json::Value = serde_json::from_str(&tree_json)
131
+ .map_err(|e| napi::Error::from_reason(format!("Invalid tree JSON: {e}")))?;
132
+
133
+ let resolved = alizarin_core::label_resolution::resolve_labels(
134
+ tree,
135
+ &alias_to_collection,
136
+ &self.inner,
137
+ strict,
138
+ )
139
+ .map_err(|e| napi::Error::from_reason(e.message))?;
140
+
141
+ Ok(resolved)
142
+ }
85
143
  }
86
144
 
87
145
  // =============================================================================
@@ -118,6 +176,181 @@ impl NapiNodeConfigManager {
118
176
  }
119
177
  }
120
178
 
179
+ // =============================================================================
180
+ // NapiTileData — Map-like interface over HashMap<String, Value>
181
+ // =============================================================================
182
+
183
+ #[napi]
184
+ pub struct NapiTileData {
185
+ entries: Arc<Mutex<HashMap<String, serde_json::Value>>>,
186
+ }
187
+
188
+ #[napi]
189
+ impl NapiTileData {
190
+ #[napi]
191
+ pub fn has(&self, key: String) -> bool {
192
+ self.entries.lock().unwrap().contains_key(&key)
193
+ }
194
+
195
+ #[napi]
196
+ pub fn get(&self, key: String) -> serde_json::Value {
197
+ self.entries
198
+ .lock()
199
+ .unwrap()
200
+ .get(&key)
201
+ .cloned()
202
+ .unwrap_or(serde_json::Value::Null)
203
+ }
204
+
205
+ #[napi]
206
+ pub fn set(&mut self, key: String, value: serde_json::Value) {
207
+ self.entries.lock().unwrap().insert(key, value);
208
+ }
209
+
210
+ #[napi]
211
+ pub fn delete(&mut self, key: String) -> bool {
212
+ self.entries.lock().unwrap().remove(&key).is_some()
213
+ }
214
+
215
+ #[napi]
216
+ pub fn keys(&self) -> Vec<String> {
217
+ self.entries.lock().unwrap().keys().cloned().collect()
218
+ }
219
+ }
220
+
221
+ // =============================================================================
222
+ // NapiStaticTile — Rust-owned tile with Map-like .data access
223
+ // =============================================================================
224
+
225
+ #[napi]
226
+ pub struct NapiStaticTile {
227
+ tileid: Option<String>,
228
+ nodegroup_id: String,
229
+ sortorder: Option<i32>,
230
+ resourceinstance_id: String,
231
+ parenttile_id: Option<String>,
232
+ provisionaledits: Option<Vec<serde_json::Value>>,
233
+ data_store: Arc<Mutex<HashMap<String, serde_json::Value>>>,
234
+ }
235
+
236
+ #[napi]
237
+ impl NapiStaticTile {
238
+ #[napi(constructor)]
239
+ pub fn new(
240
+ nodegroup_id: String,
241
+ tileid: Option<String>,
242
+ sortorder: Option<i32>,
243
+ resourceinstance_id: Option<String>,
244
+ parenttile_id: Option<String>,
245
+ ) -> Self {
246
+ NapiStaticTile {
247
+ tileid,
248
+ nodegroup_id,
249
+ sortorder,
250
+ resourceinstance_id: resourceinstance_id.unwrap_or_default(),
251
+ parenttile_id,
252
+ provisionaledits: None,
253
+ data_store: Arc::new(Mutex::new(HashMap::new())),
254
+ }
255
+ }
256
+
257
+ #[napi(getter)]
258
+ pub fn data(&self) -> NapiTileData {
259
+ NapiTileData {
260
+ entries: self.data_store.clone(),
261
+ }
262
+ }
263
+
264
+ /// Setter is a no-op — data is always accessed via the NapiTileData getter.
265
+ /// Exists to prevent TypeError in strict mode when JS does
266
+ /// `tile.data = tile.data || new Map()`.
267
+ #[napi(setter)]
268
+ pub fn set_data(&mut self, _value: serde_json::Value) {
269
+ // No-op: mutations go through NapiTileData.set/delete
270
+ }
271
+
272
+ #[napi(getter)]
273
+ pub fn tileid(&self) -> Option<String> {
274
+ self.tileid.clone()
275
+ }
276
+
277
+ #[napi(setter)]
278
+ pub fn set_tileid(&mut self, value: Option<String>) {
279
+ self.tileid = value;
280
+ }
281
+
282
+ #[napi(getter)]
283
+ pub fn nodegroup_id(&self) -> String {
284
+ self.nodegroup_id.clone()
285
+ }
286
+
287
+ #[napi(getter)]
288
+ pub fn sortorder(&self) -> Option<i32> {
289
+ self.sortorder
290
+ }
291
+
292
+ #[napi(getter)]
293
+ pub fn resourceinstance_id(&self) -> String {
294
+ self.resourceinstance_id.clone()
295
+ }
296
+
297
+ #[napi(getter)]
298
+ pub fn parenttile_id(&self) -> Option<String> {
299
+ self.parenttile_id.clone()
300
+ }
301
+
302
+ #[napi(setter)]
303
+ pub fn set_parenttile_id(&mut self, value: Option<String>) {
304
+ self.parenttile_id = value;
305
+ }
306
+
307
+ #[napi(getter)]
308
+ pub fn provisionaledits(&self) -> serde_json::Value {
309
+ match &self.provisionaledits {
310
+ Some(edits) => serde_json::to_value(edits).unwrap_or(serde_json::Value::Null),
311
+ None => serde_json::Value::Null,
312
+ }
313
+ }
314
+
315
+ /// Generate a tile ID if not already set.
316
+ #[napi(js_name = "ensureId")]
317
+ pub fn ensure_id(&mut self) -> String {
318
+ if self.tileid.is_none() {
319
+ self.tileid = Some(uuid::Uuid::new_v4().to_string());
320
+ }
321
+ self.tileid.clone().unwrap()
322
+ }
323
+ }
324
+
325
+ impl NapiStaticTile {
326
+ /// Convert to core StaticTile for storage in PseudoValueCore.
327
+ pub(crate) fn to_static_tile(&self) -> StaticTile {
328
+ let data = self.data_store.lock().unwrap().clone();
329
+ StaticTile {
330
+ tileid: self.tileid.clone(),
331
+ nodegroup_id: self.nodegroup_id.clone(),
332
+ sortorder: self.sortorder,
333
+ resourceinstance_id: self.resourceinstance_id.clone(),
334
+ parenttile_id: self.parenttile_id.clone(),
335
+ provisionaledits: self.provisionaledits.clone(),
336
+ data,
337
+ }
338
+ }
339
+
340
+ /// Create from a core StaticTile.
341
+ pub(crate) fn from_static_tile(tile: &StaticTile) -> Self {
342
+ NapiStaticTile {
343
+ tileid: tile.tileid.clone(),
344
+ nodegroup_id: tile.nodegroup_id.clone(),
345
+ sortorder: tile.sortorder,
346
+ resourceinstance_id: tile.resourceinstance_id.clone(),
347
+ parenttile_id: tile.parenttile_id.clone(),
348
+ provisionaledits: tile.provisionaledits.clone(),
349
+ data_store: Arc::new(Mutex::new(tile.data.clone())),
350
+ }
351
+ }
352
+ }
353
+
121
354
  // =============================================================================
122
355
  // NapiPseudoValue
123
356
  // =============================================================================
@@ -269,15 +502,17 @@ impl NapiPseudoValue {
269
502
  pub fn to_snapshot(&self) -> serde_json::Value {
270
503
  serde_json::json!({
271
504
  "nodeId": self.inner.node.nodeid,
505
+ "name": &self.inner.node.name,
272
506
  "alias": self.inner.node.alias,
273
507
  "datatype": &self.inner.node.datatype,
274
508
  "nodegroupId": self.inner.node.nodegroup_id,
509
+ "sortorder": self.inner.node.sortorder,
510
+ "isOuter": self.inner.is_outer(),
511
+ "isInner": self.inner.is_inner,
275
512
  "isCollector": self.inner.is_collector,
513
+ "accessed": false,
276
514
  "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,
515
+ "hasTile": self.inner.tile.is_some(),
281
516
  "tileId": self.inner.tile.as_ref().and_then(|t| t.tileid.clone()),
282
517
  "tileData": &self.inner.tile_data,
283
518
  "valueLoaded": self.inner.tile.is_some(),
@@ -290,6 +525,45 @@ impl NapiPseudoValue {
290
525
  pub fn clear(&mut self) {
291
526
  self.inner.tile_data = None;
292
527
  }
528
+
529
+ // -- Tile getter/setter (NapiStaticTile, Rust-owned) --
530
+
531
+ #[napi(getter, js_name = "tile")]
532
+ pub fn get_tile(&self) -> Option<NapiStaticTile> {
533
+ self.inner
534
+ .tile
535
+ .as_ref()
536
+ .map(|t| NapiStaticTile::from_static_tile(t.as_ref()))
537
+ }
538
+
539
+ #[napi(setter, js_name = "tile")]
540
+ pub fn set_tile(&mut self, tile: Option<&NapiStaticTile>) {
541
+ match tile {
542
+ Some(t) => {
543
+ self.inner.tile = Some(Arc::new(t.to_static_tile()));
544
+ }
545
+ None => {
546
+ self.inner.tile = None;
547
+ }
548
+ }
549
+ }
550
+
551
+ // -- Inner (outer/inner pattern) --
552
+
553
+ /// Get the inner PseudoValueCore wrapped as NapiPseudoValue.
554
+ #[napi(getter)]
555
+ pub fn inner(&self) -> Option<NapiPseudoValue> {
556
+ self.inner
557
+ .inner
558
+ .as_ref()
559
+ .map(|i| NapiPseudoValue { inner: *i.clone() })
560
+ }
561
+
562
+ /// Returns null — value is managed JS-side via _cachedValue.
563
+ #[napi(getter)]
564
+ pub fn value(&self) -> serde_json::Value {
565
+ serde_json::Value::Null
566
+ }
293
567
  }
294
568
 
295
569
  // =============================================================================
@@ -530,19 +804,19 @@ impl NapiResourceInstanceWrapper {
530
804
  // Tile loading
531
805
  // =========================================================================
532
806
 
533
- /// Load tiles from a JSON array.
807
+ /// Load tiles from a JSON string (single-pass deserialization).
534
808
  #[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)
809
+ pub fn load_tiles(&mut self, tiles_json: String) -> Result<()> {
810
+ let tiles: Vec<StaticTile> = serde_json::from_str(&tiles_json)
537
811
  .map_err(|e| napi::Error::from_reason(format!("Invalid tiles JSON: {e}")))?;
538
812
  self.inner.load_tiles(tiles);
539
813
  Ok(())
540
814
  }
541
815
 
542
- /// Load tiles directly from a StaticResource JSON.
816
+ /// Load tiles directly from a StaticResource JSON string (single-pass deserialization).
543
817
  #[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)
818
+ pub fn load_tiles_from_resource(&mut self, resource_json: String) -> Result<()> {
819
+ let resource: StaticResource = serde_json::from_str(&resource_json)
546
820
  .map_err(|e| napi::Error::from_reason(format!("Invalid resource JSON: {e}")))?;
547
821
 
548
822
  self.inner.resource_instance = Some(resource.resourceinstance.clone());
@@ -912,6 +1186,23 @@ impl NapiResourceInstanceWrapper {
912
1186
  // Path resolution
913
1187
  // =========================================================================
914
1188
 
1189
+ /// Resolve a dot-separated path to its target node metadata without needing tiles.
1190
+ ///
1191
+ /// Returns { nodegroupId, isSingle, targetNodeId } — enough for the JS layer to
1192
+ /// lazy-load just that nodegroup's tiles before calling getValuesAtPath.
1193
+ #[napi(js_name = "resolvePath")]
1194
+ pub fn resolve_path(&self, path: String) -> Result<serde_json::Value> {
1195
+ let info = self
1196
+ .inner
1197
+ .resolve_path(&path, &self.model_access)
1198
+ .map_err(|e| napi::Error::from_reason(e.to_string()))?;
1199
+ Ok(serde_json::json!({
1200
+ "nodegroupId": info.nodegroup_id,
1201
+ "isSingle": info.is_single,
1202
+ "targetNodeId": info.target_node.nodeid,
1203
+ }))
1204
+ }
1205
+
915
1206
  /// Resolve a dot-separated path and return a PseudoList.
916
1207
  #[napi]
917
1208
  pub fn get_values_at_path(&self, path: String) -> Result<NapiPseudoList> {
package/src/lib.rs CHANGED
@@ -529,6 +529,111 @@ pub fn get_registered_extension_handlers() -> Vec<String> {
529
529
  .collect()
530
530
  }
531
531
 
532
+ // ============================================================================
533
+ // SKOS parsing
534
+ // ============================================================================
535
+
536
+ /// Parse SKOS RDF/XML and return all collections as a JSON array.
537
+ #[napi(js_name = "parseSkosXml")]
538
+ pub fn parse_skos_xml(xml_content: String, base_uri: String) -> Result<serde_json::Value> {
539
+ let collections = alizarin_core::skos::parse_skos_to_collections(&xml_content, &base_uri)
540
+ .map_err(napi::Error::from_reason)?;
541
+ serde_json::to_value(&collections).map_err(|e| napi::Error::from_reason(e.to_string()))
542
+ }
543
+
544
+ /// Parse SKOS RDF/XML and return a single collection (the first one found).
545
+ #[napi(js_name = "parseSkosXmlToCollection")]
546
+ pub fn parse_skos_xml_to_collection(
547
+ xml_content: String,
548
+ base_uri: String,
549
+ ) -> Result<serde_json::Value> {
550
+ let mut collections = alizarin_core::skos::parse_skos_to_collections(&xml_content, &base_uri)
551
+ .map_err(napi::Error::from_reason)?;
552
+ if collections.is_empty() {
553
+ return Err(napi::Error::from_reason(
554
+ "No SKOS ConceptScheme found in XML".to_string(),
555
+ ));
556
+ }
557
+ let collection = collections.remove(0);
558
+ serde_json::to_value(&collection).map_err(|e| napi::Error::from_reason(e.to_string()))
559
+ }
560
+
561
+ // ============================================================================
562
+ // Label resolution utilities
563
+ // ============================================================================
564
+
565
+ /// Build a mapping from node alias to collection ID based on graph definition.
566
+ ///
567
+ /// Equivalent to WASM's `buildAliasToCollectionMap`.
568
+ #[napi(js_name = "buildAliasToCollectionMap")]
569
+ pub fn build_alias_to_collection_map(
570
+ graph_json: String,
571
+ resolvable_datatypes: Option<Vec<String>>,
572
+ config_keys: Option<Vec<String>>,
573
+ ) -> Result<HashMap<String, String>> {
574
+ let graph: serde_json::Value = serde_json::from_str(&graph_json)
575
+ .map_err(|e| napi::Error::from_reason(format!("Invalid graph JSON: {e}")))?;
576
+
577
+ let config = alizarin_core::LabelResolutionConfig {
578
+ resolvable_datatypes: resolvable_datatypes.unwrap_or_else(|| {
579
+ alizarin_core::DEFAULT_RESOLVABLE_DATATYPES
580
+ .iter()
581
+ .map(|s| s.to_string())
582
+ .collect()
583
+ }),
584
+ config_keys: config_keys.unwrap_or_else(|| {
585
+ alizarin_core::DEFAULT_CONFIG_KEYS
586
+ .iter()
587
+ .map(|s| s.to_string())
588
+ .collect()
589
+ }),
590
+ strict: false,
591
+ };
592
+
593
+ Ok(alizarin_core::build_alias_to_collection_map(
594
+ &graph, &config,
595
+ ))
596
+ }
597
+
598
+ /// Scan a JSON tree to find which collections are needed for resolution.
599
+ ///
600
+ /// Equivalent to WASM's `findNeededCollections`.
601
+ #[napi(js_name = "findNeededCollections")]
602
+ pub fn find_needed_collections(
603
+ tree_json: String,
604
+ alias_to_collection: HashMap<String, String>,
605
+ ) -> Result<Vec<String>> {
606
+ let tree: serde_json::Value = serde_json::from_str(&tree_json)
607
+ .map_err(|e| napi::Error::from_reason(format!("Invalid tree JSON: {e}")))?;
608
+
609
+ let needed = alizarin_core::find_needed_collections(&tree, &alias_to_collection);
610
+ Ok(needed.into_iter().collect())
611
+ }
612
+
613
+ /// Check if a string is a valid UUID.
614
+ #[napi(js_name = "isValidUuid")]
615
+ pub fn is_valid_uuid(s: String) -> bool {
616
+ alizarin_core::is_valid_uuid(&s)
617
+ }
618
+
619
+ /// Get the default resolvable datatypes (concept, concept-list).
620
+ #[napi(js_name = "getDefaultResolvableDatatypes")]
621
+ pub fn get_default_resolvable_datatypes() -> Vec<String> {
622
+ alizarin_core::DEFAULT_RESOLVABLE_DATATYPES
623
+ .iter()
624
+ .map(|s| s.to_string())
625
+ .collect()
626
+ }
627
+
628
+ /// Get the default config keys for collection IDs.
629
+ #[napi(js_name = "getDefaultConfigKeys")]
630
+ pub fn get_default_config_keys() -> Vec<String> {
631
+ alizarin_core::DEFAULT_CONFIG_KEYS
632
+ .iter()
633
+ .map(|s| s.to_string())
634
+ .collect()
635
+ }
636
+
532
637
  // ============================================================================
533
638
  // Prebuild import (high-level convenience)
534
639
  // ============================================================================