@alizarin/napi 2.0.0-alpha.96 → 2.0.0-alpha.98

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.
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import fs from 'node:fs';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
- import { NapiStaticGraph, NapiStaticResourceRegistry, NapiResourceModelWrapper, NapiResourceInstanceWrapper } from '../index.js';
7
+ import { NapiStaticGraph, NapiStaticResourceRegistry, NapiResourceModelWrapper, NapiResourceInstanceWrapper, NapiNodeConfigManager } from '../index.js';
8
8
 
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const TEST_DATA = path.resolve(__dirname, '..', '..', '..', 'tests');
@@ -522,3 +522,44 @@ describe('NapiResourceInstanceWrapper methods', () => {
522
522
  assert.equal(tiles.length, 0, 'should be empty');
523
523
  });
524
524
  });
525
+
526
+ // =============================================================================
527
+ // NapiNodeConfigManager + NapiStaticGraph integration
528
+ // Regression: loadFromGraph used JSON.stringify on a NapiStaticGraph, which
529
+ // produced {} because NAPI class getters are not enumerable own properties.
530
+ // The fix uses buildFromGraph (direct struct reference) instead.
531
+ // =============================================================================
532
+
533
+ describe('NapiNodeConfigManager', () => {
534
+ const personJson = fs.readFileSync(
535
+ path.join(TEST_DATA, 'data', 'models', 'Person.json'),
536
+ 'utf-8'
537
+ );
538
+
539
+ it('buildFromGraph accepts a NapiStaticGraph without error', () => {
540
+ const graph = NapiStaticGraph.fromJsonString(personJson);
541
+ const ncm = new NapiNodeConfigManager();
542
+ // This is the path that failed when using JSON.stringify(napiGraph)
543
+ ncm.buildFromGraph(graph);
544
+ });
545
+
546
+ it('buildFromGraphJson fails on JSON.stringify of a NapiStaticGraph', () => {
547
+ const graph = NapiStaticGraph.fromJsonString(personJson);
548
+ const ncm = new NapiNodeConfigManager();
549
+ // Demonstrates the bug: NAPI objects don't stringify their getters
550
+ const stringified = JSON.stringify(graph);
551
+ assert.throws(
552
+ () => ncm.buildFromGraphJson(stringified),
553
+ /Failed to parse graph/i,
554
+ 'JSON.stringify of NapiStaticGraph should produce invalid graph JSON'
555
+ );
556
+ });
557
+
558
+ it('buildFromGraphJson works with raw graph JSON', () => {
559
+ const parsed = JSON.parse(personJson);
560
+ const graphObj = parsed.graph ? parsed.graph[0] : parsed;
561
+ const ncm = new NapiNodeConfigManager();
562
+ // Raw JS object stringifies correctly
563
+ ncm.buildFromGraphJson(JSON.stringify(graphObj));
564
+ });
565
+ });
Binary file
Binary file
Binary file
Binary file
package/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare function buildGraphFromCsvs(graphCsv: string, nodesCsv: string, c
17
17
  * Returns the resources wrapped in the `{ business_data: { resources: [...] } }`
18
18
  * format expected by PrebuildLoader.
19
19
  */
20
- export declare function buildBusinessDataFromCsv(csvData: string, graphJson: string, collectionsJson: string): any
20
+ export declare function buildBusinessDataFromCsv(csvData: string, graphJson: string, collectionsJson: string, defaultLanguage?: string | undefined | null, strictConcepts?: boolean | undefined | null): any
21
21
  /**
22
22
  * Coerce a value using the registered extension handler for the given datatype.
23
23
  *
@@ -44,6 +44,10 @@ export declare function getRegisteredExtensionHandlers(): Array<string>
44
44
  export declare function parseSkosXml(xmlContent: string, baseUri: string): any
45
45
  /** Parse SKOS RDF/XML and return a single collection (the first one found). */
46
46
  export declare function parseSkosXmlToCollection(xmlContent: string, baseUri: string): any
47
+ /** Serialize a single SkosCollection to SKOS RDF/XML. */
48
+ export declare function collectionToSkosXml(collectionJson: any, baseUri: string): string
49
+ /** Serialize multiple SkosCollections to SKOS RDF/XML. */
50
+ export declare function collectionsToSkosXml(collectionsJson: any, baseUri: string): string
47
51
  /**
48
52
  * Build a mapping from node alias to collection ID based on graph definition.
49
53
  *
@@ -193,8 +197,13 @@ export declare class NapiValuesFromNodegroupResult {
193
197
  get impliedNodegroups(): Array<string>
194
198
  }
195
199
  export declare class NapiResourceInstanceWrapper {
196
- /** Create a wrapper for a given graph (must be registered). */
197
- constructor(graphId: string)
200
+ /**
201
+ * Create a wrapper for a given graph (must be registered).
202
+ *
203
+ * If `resource_id` is provided, a minimal resource metadata is created so
204
+ * the tile-source fast path can look up tiles by resource.
205
+ */
206
+ constructor(graphId: string, resourceId?: string | undefined | null)
198
207
  /** Load tiles from a JSON string (single-pass deserialization). */
199
208
  loadTiles(tilesJson: string): void
200
209
  /** Load tiles directly from a StaticResource JSON string (single-pass deserialization). */
@@ -368,6 +377,13 @@ export declare class NapiStaticGraph {
368
377
  get name(): any
369
378
  /** Register this graph in the global registry so NapiResourceInstanceWrapper can use it. */
370
379
  register(): void
380
+ /**
381
+ * Set a descriptor template for computing resource name/description/slug.
382
+ *
383
+ * Template placeholders use `<Node Name>` syntax, e.g. `"<Headword>"`.
384
+ * Descriptor types: "name", "description", "slug".
385
+ */
386
+ setDescriptorTemplate(descriptorType: string, stringTemplate: string): void
371
387
  }
372
388
  export declare class NapiStaticResourceRegistry {
373
389
  constructor()
@@ -384,7 +400,7 @@ export declare class NapiStaticResourceRegistry {
384
400
  /** Insert full resources from a business_data JSON file string. */
385
401
  mergeFromBusinessDataJson(businessDataJson: string, storeFull?: boolean | undefined | null): void
386
402
  /** Build an inverted index: visibility value -> [resource IDs]. */
387
- getValueToResourcesIndex(graph: NapiStaticGraph, nodeIdentifier: string, flattenLocalized?: boolean | undefined | null): Record<string, Array<string>>
403
+ getValueToResourcesIndex(graph: NapiStaticGraph, nodeIdentifier: string, flattenLocalized?: boolean | undefined | null, rdmCache?: NapiRdmCache | undefined | null): Record<string, Array<string>>
388
404
  /**
389
405
  * Extract values from one node in tiles where another node matches a filter.
390
406
  *
@@ -406,4 +422,17 @@ export declare class NapiStaticResourceRegistry {
406
422
  memoryStats(): any
407
423
  /** Get detailed stats including estimated byte sizes (expensive — re-serializes all data) */
408
424
  memoryStatsDetailed(): any
425
+ /**
426
+ * Populate __cache on resources with summaries for referenced resources.
427
+ *
428
+ * Uses the graph to identify resource-instance nodes, then populates
429
+ * cache entries for each referenced resource found in this registry.
430
+ *
431
+ * If `enrich_relationships` is true, also adds ontologyProperty to tile data.
432
+ * If `recompute_descriptors` is true, recomputes resource name/description from
433
+ * descriptor templates set on the graph.
434
+ *
435
+ * Returns `{ resources, unknownReferences, hasUnknown }`.
436
+ */
437
+ populateCachesFromJson(resourcesJson: string, graph: NapiStaticGraph, enrichRelationships?: boolean | undefined | null, strict?: boolean | undefined | null, recomputeDescriptors?: boolean | undefined | null): any
409
438
  }
package/index.js CHANGED
@@ -310,7 +310,7 @@ if (!nativeBinding) {
310
310
  throw new Error(`Failed to load native binding`)
311
311
  }
312
312
 
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
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, collectionToSkosXml, collectionsToSkosXml, buildAliasToCollectionMap, findNeededCollections, isValidUuid, getDefaultResolvableDatatypes, getDefaultConfigKeys, importPrebuild } = nativeBinding
314
314
 
315
315
  module.exports.NapiRdmCache = NapiRdmCache
316
316
  module.exports.NapiNodeConfigManager = NapiNodeConfigManager
@@ -335,6 +335,8 @@ module.exports.hasExtensionHandler = hasExtensionHandler
335
335
  module.exports.getRegisteredExtensionHandlers = getRegisteredExtensionHandlers
336
336
  module.exports.parseSkosXml = parseSkosXml
337
337
  module.exports.parseSkosXmlToCollection = parseSkosXmlToCollection
338
+ module.exports.collectionToSkosXml = collectionToSkosXml
339
+ module.exports.collectionsToSkosXml = collectionsToSkosXml
338
340
  module.exports.buildAliasToCollectionMap = buildAliasToCollectionMap
339
341
  module.exports.findNeededCollections = findNeededCollections
340
342
  module.exports.isValidUuid = isValidUuid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alizarin/napi",
3
- "version": "2.0.0-alpha.96",
3
+ "version": "2.0.0-alpha.98",
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.96",
34
- "@alizarin/napi-darwin-x64": "2.0.0-alpha.96",
35
- "@alizarin/napi-linux-x64-gnu": "2.0.0-alpha.96"
33
+ "@alizarin/napi-win32-x64-msvc": "2.0.0-alpha.98",
34
+ "@alizarin/napi-darwin-x64": "2.0.0-alpha.98",
35
+ "@alizarin/napi-linux-x64-gnu": "2.0.0-alpha.98"
36
36
  }
37
37
  }
@@ -44,7 +44,14 @@ fn sc_err(e: SemanticChildError) -> napi::Error {
44
44
 
45
45
  #[napi]
46
46
  pub struct NapiRdmCache {
47
- inner: RdmCache,
47
+ pub(crate) inner: RdmCache,
48
+ }
49
+
50
+ impl NapiRdmCache {
51
+ /// Crate-internal accessor (field not directly accessible through #[napi] macro).
52
+ pub(crate) fn inner(&self) -> &RdmCache {
53
+ &self.inner
54
+ }
48
55
  }
49
56
 
50
57
  #[napi]
@@ -780,8 +787,11 @@ impl NapiResourceInstanceWrapper {
780
787
  // =========================================================================
781
788
 
782
789
  /// Create a wrapper for a given graph (must be registered).
790
+ ///
791
+ /// If `resource_id` is provided, a minimal resource metadata is created so
792
+ /// the tile-source fast path can look up tiles by resource.
783
793
  #[napi(constructor)]
784
- pub fn new(graph_id: String) -> Result<Self> {
794
+ pub fn new(graph_id: String, _resource_id: Option<String>) -> Result<Self> {
785
795
  let graph = alizarin_core::get_graph(&graph_id).ok_or_else(|| {
786
796
  napi::Error::from_reason(format!(
787
797
  "Graph '{}' not registered. Call registerGraph() first.",
@@ -790,6 +800,7 @@ impl NapiResourceInstanceWrapper {
790
800
  })?;
791
801
 
792
802
  let model_access = GraphModelAccess::from_graph(&graph);
803
+ // TODO: use resource_id with new_for_resource_id once tile-source plumbing is committed
793
804
  let mut core = ResourceInstanceWrapperCore::new(graph_id);
794
805
  core.set_cached_indices(&model_access);
795
806
 
package/src/lib.rs CHANGED
@@ -1,4 +1,5 @@
1
1
  mod instance_wrapper_napi;
2
+ use instance_wrapper_napi::NapiRdmCache;
2
3
 
3
4
  use std::collections::HashMap;
4
5
  use std::sync::OnceLock;
@@ -166,6 +167,21 @@ impl NapiStaticGraph {
166
167
  pub fn register(&self) {
167
168
  alizarin_core::register_graph_owned(self.inner.clone());
168
169
  }
170
+
171
+ /// Set a descriptor template for computing resource name/description/slug.
172
+ ///
173
+ /// Template placeholders use `<Node Name>` syntax, e.g. `"<Headword>"`.
174
+ /// Descriptor types: "name", "description", "slug".
175
+ #[napi(js_name = "setDescriptorTemplate")]
176
+ pub fn set_descriptor_template(
177
+ &mut self,
178
+ descriptor_type: String,
179
+ string_template: String,
180
+ ) -> Result<()> {
181
+ self.inner
182
+ .set_descriptor_template(&descriptor_type, &string_template)
183
+ .map_err(napi::Error::from_reason)
184
+ }
169
185
  }
170
186
 
171
187
  // ============================================================================
@@ -281,9 +297,12 @@ impl NapiStaticResourceRegistry {
281
297
  graph: &NapiStaticGraph,
282
298
  node_identifier: String,
283
299
  flatten_localized: Option<bool>,
300
+ rdm_cache: Option<&NapiRdmCache>,
284
301
  ) -> Result<HashMap<String, Vec<String>>> {
285
302
  let ctx = SerializationContext {
286
303
  extension_registry: Some(extension_registry()),
304
+ external_resolver: rdm_cache
305
+ .map(|r| r.inner() as &dyn alizarin_core::type_serialization::ExternalResolver),
287
306
  ..SerializationContext::empty()
288
307
  };
289
308
  self.inner
@@ -397,6 +416,47 @@ impl NapiStaticResourceRegistry {
397
416
  serde_json::to_value(&stats)
398
417
  .map_err(|e| napi::Error::from_reason(format!("Serialization failed: {}", e)))
399
418
  }
419
+
420
+ /// Populate __cache on resources with summaries for referenced resources.
421
+ ///
422
+ /// Uses the graph to identify resource-instance nodes, then populates
423
+ /// cache entries for each referenced resource found in this registry.
424
+ ///
425
+ /// If `enrich_relationships` is true, also adds ontologyProperty to tile data.
426
+ /// If `recompute_descriptors` is true, recomputes resource name/description from
427
+ /// descriptor templates set on the graph.
428
+ ///
429
+ /// Returns `{ resources, unknownReferences, hasUnknown }`.
430
+ #[napi(js_name = "populateCachesFromJson")]
431
+ pub fn populate_caches_from_json(
432
+ &self,
433
+ resources_json: String,
434
+ graph: &NapiStaticGraph,
435
+ enrich_relationships: Option<bool>,
436
+ strict: Option<bool>,
437
+ recompute_descriptors: Option<bool>,
438
+ ) -> Result<serde_json::Value> {
439
+ let mut resources: Vec<StaticResource> = serde_json::from_str(&resources_json)
440
+ .map_err(|e| napi::Error::from_reason(format!("Failed to parse resources: {}", e)))?;
441
+
442
+ let result = self
443
+ .inner
444
+ .populate_caches(
445
+ &mut resources,
446
+ &graph.inner,
447
+ enrich_relationships.unwrap_or(true),
448
+ strict.unwrap_or(false),
449
+ recompute_descriptors.unwrap_or(false),
450
+ )
451
+ .map_err(napi::Error::from_reason)?;
452
+
453
+ let output = serde_json::json!({
454
+ "resources": resources,
455
+ "unknownReferences": result.unknown_references,
456
+ "hasUnknown": result.has_unknown_references(),
457
+ });
458
+ Ok(output)
459
+ }
400
460
  }
401
461
 
402
462
  // ============================================================================
@@ -459,15 +519,21 @@ pub fn build_business_data_from_csv(
459
519
  csv_data: String,
460
520
  graph_json: String,
461
521
  collections_json: String,
522
+ default_language: Option<String>,
523
+ strict_concepts: Option<bool>,
462
524
  ) -> Result<serde_json::Value> {
463
525
  let graph: StaticGraph = serde_json::from_str(&graph_json)
464
526
  .map_err(|e| napi::Error::from_reason(format!("Invalid graph JSON: {e}")))?;
465
527
  let collections: Vec<SkosCollection> = serde_json::from_str(&collections_json)
466
528
  .map_err(|e| napi::Error::from_reason(format!("Invalid collections JSON: {e}")))?;
467
529
 
468
- let resources =
469
- build_resources_from_business_csv(&csv_data, &graph, &collections, Default::default())
470
- .map_err(csv_err)?;
530
+ let options = alizarin_core::csv_business_data_loader::BusinessDataCsvOptions {
531
+ default_language: default_language.unwrap_or_else(|| "en".to_string()),
532
+ strict_concepts: strict_concepts.unwrap_or(true),
533
+ };
534
+
535
+ let resources = build_resources_from_business_csv(&csv_data, &graph, &collections, options)
536
+ .map_err(csv_err)?;
471
537
 
472
538
  Ok(wrap_business_data(&resources))
473
539
  }
@@ -574,6 +640,37 @@ pub fn parse_skos_xml_to_collection(
574
640
  serde_json::to_value(&collection).map_err(|e| napi::Error::from_reason(e.to_string()))
575
641
  }
576
642
 
643
+ /// Serialize a single SkosCollection to SKOS RDF/XML.
644
+ #[napi(js_name = "collectionToSkosXml")]
645
+ pub fn collection_to_skos_xml_napi(
646
+ collection_json: serde_json::Value,
647
+ base_uri: String,
648
+ ) -> Result<String> {
649
+ let collection: SkosCollection = serde_json::from_value(collection_json).map_err(|e| {
650
+ napi::Error::from_reason(format!("Failed to deserialize collection: {}", e))
651
+ })?;
652
+ Ok(alizarin_core::skos::collection_to_skos_xml(
653
+ &collection,
654
+ &base_uri,
655
+ ))
656
+ }
657
+
658
+ /// Serialize multiple SkosCollections to SKOS RDF/XML.
659
+ #[napi(js_name = "collectionsToSkosXml")]
660
+ pub fn collections_to_skos_xml_napi(
661
+ collections_json: serde_json::Value,
662
+ base_uri: String,
663
+ ) -> Result<String> {
664
+ let collections: Vec<SkosCollection> =
665
+ serde_json::from_value(collections_json).map_err(|e| {
666
+ napi::Error::from_reason(format!("Failed to deserialize collections: {}", e))
667
+ })?;
668
+ Ok(alizarin_core::skos::collections_to_skos_xml(
669
+ &collections,
670
+ &base_uri,
671
+ ))
672
+ }
673
+
577
674
  // ============================================================================
578
675
  // Label resolution utilities
579
676
  // ============================================================================