@alizarin/napi 0.2.1-alpha.85 → 2.0.0-alpha.100
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 +1 -0
- package/__test__/index.mjs +88 -8
- package/{alizarin-napi-0.2.1-alpha.85.tgz → alizarin-napi-2.0.0-alpha.100.tgz} +0 -0
- package/alizarin-napi.darwin-arm64.node +0 -0
- package/alizarin-napi.darwin-x64.node +0 -0
- package/alizarin-napi.linux-x64-gnu.node +0 -0
- package/alizarin-napi.win32-x64-msvc.node +0 -0
- package/index.d.ts +120 -8
- package/index.js +12 -1
- package/package.json +4 -4
- package/src/instance_wrapper_napi.rs +336 -13
- package/src/lib.rs +222 -4
package/Cargo.toml
CHANGED
package/__test__/index.mjs
CHANGED
|
@@ -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');
|
|
@@ -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];
|
|
@@ -522,3 +522,83 @@ 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
|
+
});
|
|
566
|
+
|
|
567
|
+
// =============================================================================
|
|
568
|
+
// NapiStaticGraph serialization guard
|
|
569
|
+
// Regression: JSON.stringify(NapiStaticGraph) produces {} because NAPI class
|
|
570
|
+
// getters are not enumerable own properties. Any code path that does
|
|
571
|
+
// JSON.stringify(graph) where graph could be a NapiStaticGraph will break.
|
|
572
|
+
// =============================================================================
|
|
573
|
+
|
|
574
|
+
describe('NapiStaticGraph serialization', () => {
|
|
575
|
+
const groupJson = fs.readFileSync(
|
|
576
|
+
path.join(TEST_DATA, 'data', 'models', 'Group.json'),
|
|
577
|
+
'utf-8'
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
it('JSON.stringify of NapiStaticGraph produces empty object', () => {
|
|
581
|
+
const graph = NapiStaticGraph.fromJsonString(groupJson);
|
|
582
|
+
const stringified = JSON.stringify(graph);
|
|
583
|
+
// This documents the fundamental limitation: NAPI class instances
|
|
584
|
+
// do not serialize via JSON.stringify
|
|
585
|
+
assert.equal(stringified, '{}', 'NapiStaticGraph should stringify to {}');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('NapiResourceModelWrapper.fromGraph accepts NapiStaticGraph directly', () => {
|
|
589
|
+
const graph = NapiStaticGraph.fromJsonString(groupJson);
|
|
590
|
+
// This is the correct NAPI path — no JSON.stringify needed
|
|
591
|
+
const wrapper = NapiResourceModelWrapper.fromGraph(graph, true);
|
|
592
|
+
assert.equal(wrapper.getGraphId(), '07883c9e-b25c-11e9-975a-a4d18cec433a');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('NapiResourceModelWrapper constructor fails with JSON.stringify of NapiStaticGraph', () => {
|
|
596
|
+
const graph = NapiStaticGraph.fromJsonString(groupJson);
|
|
597
|
+
// Demonstrates the bug this regression test guards against
|
|
598
|
+
assert.throws(
|
|
599
|
+
() => new NapiResourceModelWrapper(JSON.stringify(graph), true),
|
|
600
|
+
/Invalid graph JSON/i,
|
|
601
|
+
'JSON.stringify of NapiStaticGraph should produce invalid input for constructor'
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
Binary file
|
|
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
|
*
|
|
@@ -40,6 +40,32 @@ 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
|
+
/** 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
|
|
51
|
+
/**
|
|
52
|
+
* Build a mapping from node alias to collection ID based on graph definition.
|
|
53
|
+
*
|
|
54
|
+
* Equivalent to WASM's `buildAliasToCollectionMap`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildAliasToCollectionMap(graphJson: string, resolvableDatatypes?: Array<string> | undefined | null, configKeys?: Array<string> | undefined | null): Record<string, string>
|
|
57
|
+
/**
|
|
58
|
+
* Scan a JSON tree to find which collections are needed for resolution.
|
|
59
|
+
*
|
|
60
|
+
* Equivalent to WASM's `findNeededCollections`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function findNeededCollections(treeJson: string, aliasToCollection: Record<string, string>): Array<string>
|
|
63
|
+
/** Check if a string is a valid UUID. */
|
|
64
|
+
export declare function isValidUuid(s: string): boolean
|
|
65
|
+
/** Get the default resolvable datatypes (concept, concept-list). */
|
|
66
|
+
export declare function getDefaultResolvableDatatypes(): Array<string>
|
|
67
|
+
/** Get the default config keys for collection IDs. */
|
|
68
|
+
export declare function getDefaultConfigKeys(): Array<string>
|
|
43
69
|
/**
|
|
44
70
|
* Import a prebuild/pkg directory: register graphs, load SKOS collections,
|
|
45
71
|
* and load ontology configs.
|
|
@@ -58,6 +84,18 @@ export declare class NapiRdmCache {
|
|
|
58
84
|
/** Load a single collection from JSON. */
|
|
59
85
|
loadCollectionJson(jsonStr: string): void
|
|
60
86
|
get collectionCount(): number
|
|
87
|
+
/** Check if a collection is already in the cache. */
|
|
88
|
+
hasCollection(collectionId: string): boolean
|
|
89
|
+
/** Add a collection from a JSON string of concepts. */
|
|
90
|
+
addCollectionFromJson(collectionId: string, jsonStr: string): void
|
|
91
|
+
/** Get the parent concept ID for a concept within a collection. */
|
|
92
|
+
getParentId(collectionId: string, conceptId: string): string | null
|
|
93
|
+
/** Remove a collection from the cache. */
|
|
94
|
+
removeCollection(collectionId: string): boolean
|
|
95
|
+
/** Clear all collections from the cache. */
|
|
96
|
+
clear(): void
|
|
97
|
+
/** Resolve labels to UUIDs using this cache for lookups. */
|
|
98
|
+
resolveLabels(treeJson: string, aliasToCollection: Record<string, string>, strict: boolean): any
|
|
61
99
|
}
|
|
62
100
|
export declare class NapiNodeConfigManager {
|
|
63
101
|
constructor()
|
|
@@ -66,6 +104,33 @@ export declare class NapiNodeConfigManager {
|
|
|
66
104
|
/** Build node configs from a NapiStaticGraph. */
|
|
67
105
|
buildFromGraph(graph: NapiStaticGraph): void
|
|
68
106
|
}
|
|
107
|
+
export declare class NapiTileData {
|
|
108
|
+
has(key: string): boolean
|
|
109
|
+
get(key: string): any
|
|
110
|
+
set(key: string, value: any): void
|
|
111
|
+
delete(key: string): boolean
|
|
112
|
+
keys(): Array<string>
|
|
113
|
+
}
|
|
114
|
+
export declare class NapiStaticTile {
|
|
115
|
+
constructor(nodegroupId: string, tileid?: string | undefined | null, sortorder?: number | undefined | null, resourceinstanceId?: string | undefined | null, parenttileId?: string | undefined | null)
|
|
116
|
+
get data(): NapiTileData
|
|
117
|
+
/**
|
|
118
|
+
* Setter is a no-op — data is always accessed via the NapiTileData getter.
|
|
119
|
+
* Exists to prevent TypeError in strict mode when JS does
|
|
120
|
+
* `tile.data = tile.data || new Map()`.
|
|
121
|
+
*/
|
|
122
|
+
set data(value: any)
|
|
123
|
+
get tileid(): string | null
|
|
124
|
+
set tileid(value?: string | undefined | null)
|
|
125
|
+
get nodegroupId(): string
|
|
126
|
+
get sortorder(): number | null
|
|
127
|
+
get resourceinstanceId(): string
|
|
128
|
+
get parenttileId(): string | null
|
|
129
|
+
set parenttileId(value?: string | undefined | null)
|
|
130
|
+
get provisionaledits(): any
|
|
131
|
+
/** Generate a tile ID if not already set. */
|
|
132
|
+
ensureId(): string
|
|
133
|
+
}
|
|
69
134
|
export declare class NapiPseudoValue {
|
|
70
135
|
get node(): any
|
|
71
136
|
get nodeId(): string | null
|
|
@@ -96,6 +161,12 @@ export declare class NapiPseudoValue {
|
|
|
96
161
|
toSnapshot(): any
|
|
97
162
|
/** Clear tile data */
|
|
98
163
|
clear(): void
|
|
164
|
+
get tile(): NapiStaticTile | null
|
|
165
|
+
set tile(tile?: NapiStaticTile | undefined | null)
|
|
166
|
+
/** Get the inner PseudoValueCore wrapped as NapiPseudoValue. */
|
|
167
|
+
get inner(): NapiPseudoValue | null
|
|
168
|
+
/** Returns null — value is managed JS-side via _cachedValue. */
|
|
169
|
+
get value(): any
|
|
99
170
|
}
|
|
100
171
|
export declare class NapiPseudoList {
|
|
101
172
|
get nodeAlias(): string
|
|
@@ -126,12 +197,22 @@ export declare class NapiValuesFromNodegroupResult {
|
|
|
126
197
|
get impliedNodegroups(): Array<string>
|
|
127
198
|
}
|
|
128
199
|
export declare class NapiResourceInstanceWrapper {
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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)
|
|
207
|
+
/** Load tiles from a JSON string (single-pass deserialization). */
|
|
208
|
+
loadTiles(tilesJson: string): void
|
|
209
|
+
/** Load tiles directly from a StaticResource JSON string (single-pass deserialization). */
|
|
210
|
+
loadTilesFromResource(resourceJson: string): void
|
|
211
|
+
/**
|
|
212
|
+
* Load tiles directly from a NapiStaticResourceRegistry by resource ID.
|
|
213
|
+
* This avoids the JS round-trip of serializing tiles to JS and back.
|
|
214
|
+
*/
|
|
215
|
+
loadFromRegistry(resourceId: string, registry: NapiStaticResourceRegistry): boolean
|
|
135
216
|
/** Append tiles incrementally (for lazy loading). */
|
|
136
217
|
appendTiles(tilesJs: any): void
|
|
137
218
|
getTileCount(): number
|
|
@@ -169,6 +250,13 @@ export declare class NapiResourceInstanceWrapper {
|
|
|
169
250
|
ensureNodegroup(allValuesJs: any, allNodegroupsJs: any, nodegroupId: string, addIfMissing: boolean, nodegroupPermissionsJs: any, doImpliedNodegroups: boolean): NapiEnsureNodegroupResult
|
|
170
251
|
/** Build pseudo values from tiles for a specific nodegroup. */
|
|
171
252
|
valuesFromResourceNodegroup(existingValuesJs: any, nodegroupTileIds: Array<string>, nodegroupId: string): NapiValuesFromNodegroupResult
|
|
253
|
+
/**
|
|
254
|
+
* Resolve a dot-separated path to its target node metadata without needing tiles.
|
|
255
|
+
*
|
|
256
|
+
* Returns { nodegroupId, isSingle, targetNodeId } — enough for the JS layer to
|
|
257
|
+
* lazy-load just that nodegroup's tiles before calling getValuesAtPath.
|
|
258
|
+
*/
|
|
259
|
+
resolvePath(path: string): any
|
|
172
260
|
/** Resolve a dot-separated path and return a PseudoList. */
|
|
173
261
|
getValuesAtPath(path: string): NapiPseudoList
|
|
174
262
|
/**
|
|
@@ -289,6 +377,13 @@ export declare class NapiStaticGraph {
|
|
|
289
377
|
get name(): any
|
|
290
378
|
/** Register this graph in the global registry so NapiResourceInstanceWrapper can use it. */
|
|
291
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
|
|
292
387
|
}
|
|
293
388
|
export declare class NapiStaticResourceRegistry {
|
|
294
389
|
constructor()
|
|
@@ -305,7 +400,7 @@ export declare class NapiStaticResourceRegistry {
|
|
|
305
400
|
/** Insert full resources from a business_data JSON file string. */
|
|
306
401
|
mergeFromBusinessDataJson(businessDataJson: string, storeFull?: boolean | undefined | null): void
|
|
307
402
|
/** Build an inverted index: visibility value -> [resource IDs]. */
|
|
308
|
-
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>>
|
|
309
404
|
/**
|
|
310
405
|
* Extract values from one node in tiles where another node matches a filter.
|
|
311
406
|
*
|
|
@@ -323,4 +418,21 @@ export declare class NapiStaticResourceRegistry {
|
|
|
323
418
|
getFull(resourceId: string): any | null
|
|
324
419
|
/** Get a summary for a resource (works for both summary and full entries), or null if unknown. */
|
|
325
420
|
getSummary(resourceId: string): any | null
|
|
421
|
+
/** Get diagnostic stats about registry contents (entry counts, tile counts, etc.) */
|
|
422
|
+
memoryStats(): any
|
|
423
|
+
/** Get detailed stats including estimated byte sizes (expensive — re-serializes all data) */
|
|
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
|
|
326
438
|
}
|
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, collectionToSkosXml, collectionsToSkosXml, 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,13 @@ 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.collectionToSkosXml = collectionToSkosXml
|
|
339
|
+
module.exports.collectionsToSkosXml = collectionsToSkosXml
|
|
340
|
+
module.exports.buildAliasToCollectionMap = buildAliasToCollectionMap
|
|
341
|
+
module.exports.findNeededCollections = findNeededCollections
|
|
342
|
+
module.exports.isValidUuid = isValidUuid
|
|
343
|
+
module.exports.getDefaultResolvableDatatypes = getDefaultResolvableDatatypes
|
|
344
|
+
module.exports.getDefaultConfigKeys = getDefaultConfigKeys
|
|
334
345
|
module.exports.importPrebuild = importPrebuild
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alizarin/napi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0-alpha.100",
|
|
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": "0.
|
|
34
|
-
"@alizarin/napi-darwin-x64": "0.
|
|
35
|
-
"@alizarin/napi-linux-x64-gnu": "0.
|
|
33
|
+
"@alizarin/napi-win32-x64-msvc": "2.0.0-alpha.100",
|
|
34
|
+
"@alizarin/napi-darwin-x64": "2.0.0-alpha.100",
|
|
35
|
+
"@alizarin/napi-linux-x64-gnu": "2.0.0-alpha.100"
|
|
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;
|
|
@@ -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]
|
|
@@ -82,6 +89,64 @@ impl NapiRdmCache {
|
|
|
82
89
|
pub fn collection_count(&self) -> u32 {
|
|
83
90
|
self.inner.len() as u32
|
|
84
91
|
}
|
|
92
|
+
|
|
93
|
+
/// Check if a collection is already in the cache.
|
|
94
|
+
#[napi]
|
|
95
|
+
pub fn has_collection(&self, collection_id: String) -> bool {
|
|
96
|
+
self.inner.has_collection(&collection_id)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Add a collection from a JSON string of concepts.
|
|
100
|
+
#[napi]
|
|
101
|
+
pub fn add_collection_from_json(
|
|
102
|
+
&mut self,
|
|
103
|
+
collection_id: String,
|
|
104
|
+
json_str: String,
|
|
105
|
+
) -> Result<()> {
|
|
106
|
+
self.inner
|
|
107
|
+
.add_collection_from_json(&collection_id, &json_str)
|
|
108
|
+
.map_err(|e| napi::Error::from_reason(format!("Failed to add collection: {e}")))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Get the parent concept ID for a concept within a collection.
|
|
112
|
+
#[napi]
|
|
113
|
+
pub fn get_parent_id(&self, collection_id: String, concept_id: String) -> Option<String> {
|
|
114
|
+
self.inner.get_parent_id(&collection_id, &concept_id)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Remove a collection from the cache.
|
|
118
|
+
#[napi]
|
|
119
|
+
pub fn remove_collection(&mut self, collection_id: String) -> bool {
|
|
120
|
+
self.inner.remove_collection(&collection_id)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Clear all collections from the cache.
|
|
124
|
+
#[napi]
|
|
125
|
+
pub fn clear(&mut self) {
|
|
126
|
+
self.inner.clear();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Resolve labels to UUIDs using this cache for lookups.
|
|
130
|
+
#[napi]
|
|
131
|
+
pub fn resolve_labels(
|
|
132
|
+
&self,
|
|
133
|
+
tree_json: String,
|
|
134
|
+
alias_to_collection: HashMap<String, String>,
|
|
135
|
+
strict: bool,
|
|
136
|
+
) -> Result<serde_json::Value> {
|
|
137
|
+
let tree: serde_json::Value = serde_json::from_str(&tree_json)
|
|
138
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid tree JSON: {e}")))?;
|
|
139
|
+
|
|
140
|
+
let resolved = alizarin_core::label_resolution::resolve_labels(
|
|
141
|
+
tree,
|
|
142
|
+
&alias_to_collection,
|
|
143
|
+
&self.inner,
|
|
144
|
+
strict,
|
|
145
|
+
)
|
|
146
|
+
.map_err(|e| napi::Error::from_reason(e.message))?;
|
|
147
|
+
|
|
148
|
+
Ok(resolved)
|
|
149
|
+
}
|
|
85
150
|
}
|
|
86
151
|
|
|
87
152
|
// =============================================================================
|
|
@@ -118,6 +183,181 @@ impl NapiNodeConfigManager {
|
|
|
118
183
|
}
|
|
119
184
|
}
|
|
120
185
|
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// NapiTileData — Map-like interface over HashMap<String, Value>
|
|
188
|
+
// =============================================================================
|
|
189
|
+
|
|
190
|
+
#[napi]
|
|
191
|
+
pub struct NapiTileData {
|
|
192
|
+
entries: Arc<Mutex<HashMap<String, serde_json::Value>>>,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[napi]
|
|
196
|
+
impl NapiTileData {
|
|
197
|
+
#[napi]
|
|
198
|
+
pub fn has(&self, key: String) -> bool {
|
|
199
|
+
self.entries.lock().unwrap().contains_key(&key)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[napi]
|
|
203
|
+
pub fn get(&self, key: String) -> serde_json::Value {
|
|
204
|
+
self.entries
|
|
205
|
+
.lock()
|
|
206
|
+
.unwrap()
|
|
207
|
+
.get(&key)
|
|
208
|
+
.cloned()
|
|
209
|
+
.unwrap_or(serde_json::Value::Null)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[napi]
|
|
213
|
+
pub fn set(&mut self, key: String, value: serde_json::Value) {
|
|
214
|
+
self.entries.lock().unwrap().insert(key, value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[napi]
|
|
218
|
+
pub fn delete(&mut self, key: String) -> bool {
|
|
219
|
+
self.entries.lock().unwrap().remove(&key).is_some()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[napi]
|
|
223
|
+
pub fn keys(&self) -> Vec<String> {
|
|
224
|
+
self.entries.lock().unwrap().keys().cloned().collect()
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// =============================================================================
|
|
229
|
+
// NapiStaticTile — Rust-owned tile with Map-like .data access
|
|
230
|
+
// =============================================================================
|
|
231
|
+
|
|
232
|
+
#[napi]
|
|
233
|
+
pub struct NapiStaticTile {
|
|
234
|
+
tileid: Option<String>,
|
|
235
|
+
nodegroup_id: String,
|
|
236
|
+
sortorder: Option<i32>,
|
|
237
|
+
resourceinstance_id: String,
|
|
238
|
+
parenttile_id: Option<String>,
|
|
239
|
+
provisionaledits: Option<Vec<serde_json::Value>>,
|
|
240
|
+
data_store: Arc<Mutex<HashMap<String, serde_json::Value>>>,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[napi]
|
|
244
|
+
impl NapiStaticTile {
|
|
245
|
+
#[napi(constructor)]
|
|
246
|
+
pub fn new(
|
|
247
|
+
nodegroup_id: String,
|
|
248
|
+
tileid: Option<String>,
|
|
249
|
+
sortorder: Option<i32>,
|
|
250
|
+
resourceinstance_id: Option<String>,
|
|
251
|
+
parenttile_id: Option<String>,
|
|
252
|
+
) -> Self {
|
|
253
|
+
NapiStaticTile {
|
|
254
|
+
tileid,
|
|
255
|
+
nodegroup_id,
|
|
256
|
+
sortorder,
|
|
257
|
+
resourceinstance_id: resourceinstance_id.unwrap_or_default(),
|
|
258
|
+
parenttile_id,
|
|
259
|
+
provisionaledits: None,
|
|
260
|
+
data_store: Arc::new(Mutex::new(HashMap::new())),
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[napi(getter)]
|
|
265
|
+
pub fn data(&self) -> NapiTileData {
|
|
266
|
+
NapiTileData {
|
|
267
|
+
entries: self.data_store.clone(),
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Setter is a no-op — data is always accessed via the NapiTileData getter.
|
|
272
|
+
/// Exists to prevent TypeError in strict mode when JS does
|
|
273
|
+
/// `tile.data = tile.data || new Map()`.
|
|
274
|
+
#[napi(setter)]
|
|
275
|
+
pub fn set_data(&mut self, _value: serde_json::Value) {
|
|
276
|
+
// No-op: mutations go through NapiTileData.set/delete
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#[napi(getter)]
|
|
280
|
+
pub fn tileid(&self) -> Option<String> {
|
|
281
|
+
self.tileid.clone()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[napi(setter)]
|
|
285
|
+
pub fn set_tileid(&mut self, value: Option<String>) {
|
|
286
|
+
self.tileid = value;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#[napi(getter)]
|
|
290
|
+
pub fn nodegroup_id(&self) -> String {
|
|
291
|
+
self.nodegroup_id.clone()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[napi(getter)]
|
|
295
|
+
pub fn sortorder(&self) -> Option<i32> {
|
|
296
|
+
self.sortorder
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#[napi(getter)]
|
|
300
|
+
pub fn resourceinstance_id(&self) -> String {
|
|
301
|
+
self.resourceinstance_id.clone()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[napi(getter)]
|
|
305
|
+
pub fn parenttile_id(&self) -> Option<String> {
|
|
306
|
+
self.parenttile_id.clone()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[napi(setter)]
|
|
310
|
+
pub fn set_parenttile_id(&mut self, value: Option<String>) {
|
|
311
|
+
self.parenttile_id = value;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[napi(getter)]
|
|
315
|
+
pub fn provisionaledits(&self) -> serde_json::Value {
|
|
316
|
+
match &self.provisionaledits {
|
|
317
|
+
Some(edits) => serde_json::to_value(edits).unwrap_or(serde_json::Value::Null),
|
|
318
|
+
None => serde_json::Value::Null,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// Generate a tile ID if not already set.
|
|
323
|
+
#[napi(js_name = "ensureId")]
|
|
324
|
+
pub fn ensure_id(&mut self) -> String {
|
|
325
|
+
if self.tileid.is_none() {
|
|
326
|
+
self.tileid = Some(uuid::Uuid::new_v4().to_string());
|
|
327
|
+
}
|
|
328
|
+
self.tileid.clone().unwrap()
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
impl NapiStaticTile {
|
|
333
|
+
/// Convert to core StaticTile for storage in PseudoValueCore.
|
|
334
|
+
pub(crate) fn to_static_tile(&self) -> StaticTile {
|
|
335
|
+
let data = self.data_store.lock().unwrap().clone();
|
|
336
|
+
StaticTile {
|
|
337
|
+
tileid: self.tileid.clone(),
|
|
338
|
+
nodegroup_id: self.nodegroup_id.clone(),
|
|
339
|
+
sortorder: self.sortorder,
|
|
340
|
+
resourceinstance_id: self.resourceinstance_id.clone(),
|
|
341
|
+
parenttile_id: self.parenttile_id.clone(),
|
|
342
|
+
provisionaledits: self.provisionaledits.clone(),
|
|
343
|
+
data,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Create from a core StaticTile.
|
|
348
|
+
pub(crate) fn from_static_tile(tile: &StaticTile) -> Self {
|
|
349
|
+
NapiStaticTile {
|
|
350
|
+
tileid: tile.tileid.clone(),
|
|
351
|
+
nodegroup_id: tile.nodegroup_id.clone(),
|
|
352
|
+
sortorder: tile.sortorder,
|
|
353
|
+
resourceinstance_id: tile.resourceinstance_id.clone(),
|
|
354
|
+
parenttile_id: tile.parenttile_id.clone(),
|
|
355
|
+
provisionaledits: tile.provisionaledits.clone(),
|
|
356
|
+
data_store: Arc::new(Mutex::new(tile.data.clone())),
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
121
361
|
// =============================================================================
|
|
122
362
|
// NapiPseudoValue
|
|
123
363
|
// =============================================================================
|
|
@@ -269,15 +509,17 @@ impl NapiPseudoValue {
|
|
|
269
509
|
pub fn to_snapshot(&self) -> serde_json::Value {
|
|
270
510
|
serde_json::json!({
|
|
271
511
|
"nodeId": self.inner.node.nodeid,
|
|
512
|
+
"name": &self.inner.node.name,
|
|
272
513
|
"alias": self.inner.node.alias,
|
|
273
514
|
"datatype": &self.inner.node.datatype,
|
|
274
515
|
"nodegroupId": self.inner.node.nodegroup_id,
|
|
516
|
+
"sortorder": self.inner.node.sortorder,
|
|
517
|
+
"isOuter": self.inner.is_outer(),
|
|
518
|
+
"isInner": self.inner.is_inner,
|
|
275
519
|
"isCollector": self.inner.is_collector,
|
|
520
|
+
"accessed": false,
|
|
276
521
|
"independent": self.inner.independent,
|
|
277
|
-
"
|
|
278
|
-
"isrequired": self.inner.node.isrequired,
|
|
279
|
-
"issearchable": self.inner.node.issearchable,
|
|
280
|
-
"hascustomalias": self.inner.node.hascustomalias,
|
|
522
|
+
"hasTile": self.inner.tile.is_some(),
|
|
281
523
|
"tileId": self.inner.tile.as_ref().and_then(|t| t.tileid.clone()),
|
|
282
524
|
"tileData": &self.inner.tile_data,
|
|
283
525
|
"valueLoaded": self.inner.tile.is_some(),
|
|
@@ -290,6 +532,45 @@ impl NapiPseudoValue {
|
|
|
290
532
|
pub fn clear(&mut self) {
|
|
291
533
|
self.inner.tile_data = None;
|
|
292
534
|
}
|
|
535
|
+
|
|
536
|
+
// -- Tile getter/setter (NapiStaticTile, Rust-owned) --
|
|
537
|
+
|
|
538
|
+
#[napi(getter, js_name = "tile")]
|
|
539
|
+
pub fn get_tile(&self) -> Option<NapiStaticTile> {
|
|
540
|
+
self.inner
|
|
541
|
+
.tile
|
|
542
|
+
.as_ref()
|
|
543
|
+
.map(|t| NapiStaticTile::from_static_tile(t.as_ref()))
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
#[napi(setter, js_name = "tile")]
|
|
547
|
+
pub fn set_tile(&mut self, tile: Option<&NapiStaticTile>) {
|
|
548
|
+
match tile {
|
|
549
|
+
Some(t) => {
|
|
550
|
+
self.inner.tile = Some(Arc::new(t.to_static_tile()));
|
|
551
|
+
}
|
|
552
|
+
None => {
|
|
553
|
+
self.inner.tile = None;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// -- Inner (outer/inner pattern) --
|
|
559
|
+
|
|
560
|
+
/// Get the inner PseudoValueCore wrapped as NapiPseudoValue.
|
|
561
|
+
#[napi(getter)]
|
|
562
|
+
pub fn inner(&self) -> Option<NapiPseudoValue> {
|
|
563
|
+
self.inner
|
|
564
|
+
.inner
|
|
565
|
+
.as_ref()
|
|
566
|
+
.map(|i| NapiPseudoValue { inner: *i.clone() })
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/// Returns null — value is managed JS-side via _cachedValue.
|
|
570
|
+
#[napi(getter)]
|
|
571
|
+
pub fn value(&self) -> serde_json::Value {
|
|
572
|
+
serde_json::Value::Null
|
|
573
|
+
}
|
|
293
574
|
}
|
|
294
575
|
|
|
295
576
|
// =============================================================================
|
|
@@ -506,8 +787,11 @@ impl NapiResourceInstanceWrapper {
|
|
|
506
787
|
// =========================================================================
|
|
507
788
|
|
|
508
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.
|
|
509
793
|
#[napi(constructor)]
|
|
510
|
-
pub fn new(graph_id: String) -> Result<Self> {
|
|
794
|
+
pub fn new(graph_id: String, _resource_id: Option<String>) -> Result<Self> {
|
|
511
795
|
let graph = alizarin_core::get_graph(&graph_id).ok_or_else(|| {
|
|
512
796
|
napi::Error::from_reason(format!(
|
|
513
797
|
"Graph '{}' not registered. Call registerGraph() first.",
|
|
@@ -516,6 +800,7 @@ impl NapiResourceInstanceWrapper {
|
|
|
516
800
|
})?;
|
|
517
801
|
|
|
518
802
|
let model_access = GraphModelAccess::from_graph(&graph);
|
|
803
|
+
// TODO: use resource_id with new_for_resource_id once tile-source plumbing is committed
|
|
519
804
|
let mut core = ResourceInstanceWrapperCore::new(graph_id);
|
|
520
805
|
core.set_cached_indices(&model_access);
|
|
521
806
|
|
|
@@ -530,19 +815,19 @@ impl NapiResourceInstanceWrapper {
|
|
|
530
815
|
// Tile loading
|
|
531
816
|
// =========================================================================
|
|
532
817
|
|
|
533
|
-
/// Load tiles from a JSON
|
|
818
|
+
/// Load tiles from a JSON string (single-pass deserialization).
|
|
534
819
|
#[napi]
|
|
535
|
-
pub fn load_tiles(&mut self,
|
|
536
|
-
let tiles: Vec<StaticTile> = serde_json::
|
|
820
|
+
pub fn load_tiles(&mut self, tiles_json: String) -> Result<()> {
|
|
821
|
+
let tiles: Vec<StaticTile> = serde_json::from_str(&tiles_json)
|
|
537
822
|
.map_err(|e| napi::Error::from_reason(format!("Invalid tiles JSON: {e}")))?;
|
|
538
823
|
self.inner.load_tiles(tiles);
|
|
539
824
|
Ok(())
|
|
540
825
|
}
|
|
541
826
|
|
|
542
|
-
/// Load tiles directly from a StaticResource JSON.
|
|
827
|
+
/// Load tiles directly from a StaticResource JSON string (single-pass deserialization).
|
|
543
828
|
#[napi]
|
|
544
|
-
pub fn load_tiles_from_resource(&mut self,
|
|
545
|
-
let resource: StaticResource = serde_json::
|
|
829
|
+
pub fn load_tiles_from_resource(&mut self, resource_json: String) -> Result<()> {
|
|
830
|
+
let resource: StaticResource = serde_json::from_str(&resource_json)
|
|
546
831
|
.map_err(|e| napi::Error::from_reason(format!("Invalid resource JSON: {e}")))?;
|
|
547
832
|
|
|
548
833
|
self.inner.resource_instance = Some(resource.resourceinstance.clone());
|
|
@@ -553,6 +838,27 @@ impl NapiResourceInstanceWrapper {
|
|
|
553
838
|
Ok(())
|
|
554
839
|
}
|
|
555
840
|
|
|
841
|
+
/// Load tiles directly from a NapiStaticResourceRegistry by resource ID.
|
|
842
|
+
/// This avoids the JS round-trip of serializing tiles to JS and back.
|
|
843
|
+
#[napi]
|
|
844
|
+
pub fn load_from_registry(
|
|
845
|
+
&mut self,
|
|
846
|
+
resource_id: String,
|
|
847
|
+
registry: &crate::NapiStaticResourceRegistry,
|
|
848
|
+
) -> Result<bool> {
|
|
849
|
+
let resource = match registry.inner.get_full(&resource_id) {
|
|
850
|
+
Some(r) => r,
|
|
851
|
+
None => return Ok(false),
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
self.inner.resource_instance = Some(resource.resourceinstance.clone());
|
|
855
|
+
|
|
856
|
+
if let Some(tiles_vec) = &resource.tiles {
|
|
857
|
+
self.inner.load_tiles(tiles_vec.clone());
|
|
858
|
+
}
|
|
859
|
+
Ok(true)
|
|
860
|
+
}
|
|
861
|
+
|
|
556
862
|
/// Append tiles incrementally (for lazy loading).
|
|
557
863
|
#[napi]
|
|
558
864
|
pub fn append_tiles(&mut self, tiles_js: serde_json::Value) -> Result<()> {
|
|
@@ -891,6 +1197,23 @@ impl NapiResourceInstanceWrapper {
|
|
|
891
1197
|
// Path resolution
|
|
892
1198
|
// =========================================================================
|
|
893
1199
|
|
|
1200
|
+
/// Resolve a dot-separated path to its target node metadata without needing tiles.
|
|
1201
|
+
///
|
|
1202
|
+
/// Returns { nodegroupId, isSingle, targetNodeId } — enough for the JS layer to
|
|
1203
|
+
/// lazy-load just that nodegroup's tiles before calling getValuesAtPath.
|
|
1204
|
+
#[napi(js_name = "resolvePath")]
|
|
1205
|
+
pub fn resolve_path(&self, path: String) -> Result<serde_json::Value> {
|
|
1206
|
+
let info = self
|
|
1207
|
+
.inner
|
|
1208
|
+
.resolve_path(&path, &self.model_access)
|
|
1209
|
+
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
|
1210
|
+
Ok(serde_json::json!({
|
|
1211
|
+
"nodegroupId": info.nodegroup_id,
|
|
1212
|
+
"isSingle": info.is_single,
|
|
1213
|
+
"targetNodeId": info.target_node.nodeid,
|
|
1214
|
+
}))
|
|
1215
|
+
}
|
|
1216
|
+
|
|
894
1217
|
/// Resolve a dot-separated path and return a PseudoList.
|
|
895
1218
|
#[napi]
|
|
896
1219
|
pub fn get_values_at_path(&self, path: String) -> Result<NapiPseudoList> {
|
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
|
// ============================================================================
|
|
@@ -174,7 +190,7 @@ impl NapiStaticGraph {
|
|
|
174
190
|
|
|
175
191
|
#[napi]
|
|
176
192
|
pub struct NapiStaticResourceRegistry {
|
|
177
|
-
inner: StaticResourceRegistry,
|
|
193
|
+
pub(crate) inner: StaticResourceRegistry,
|
|
178
194
|
}
|
|
179
195
|
|
|
180
196
|
impl Default for NapiStaticResourceRegistry {
|
|
@@ -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
|
|
@@ -381,6 +400,63 @@ impl NapiStaticResourceRegistry {
|
|
|
381
400
|
None => Ok(None),
|
|
382
401
|
}
|
|
383
402
|
}
|
|
403
|
+
|
|
404
|
+
/// Get diagnostic stats about registry contents (entry counts, tile counts, etc.)
|
|
405
|
+
#[napi]
|
|
406
|
+
pub fn memory_stats(&self) -> Result<serde_json::Value> {
|
|
407
|
+
let stats = self.inner.memory_stats();
|
|
408
|
+
serde_json::to_value(&stats)
|
|
409
|
+
.map_err(|e| napi::Error::from_reason(format!("Serialization failed: {}", e)))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/// Get detailed stats including estimated byte sizes (expensive — re-serializes all data)
|
|
413
|
+
#[napi]
|
|
414
|
+
pub fn memory_stats_detailed(&self) -> Result<serde_json::Value> {
|
|
415
|
+
let stats = self.inner.memory_stats_detailed();
|
|
416
|
+
serde_json::to_value(&stats)
|
|
417
|
+
.map_err(|e| napi::Error::from_reason(format!("Serialization failed: {}", e)))
|
|
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
|
+
}
|
|
384
460
|
}
|
|
385
461
|
|
|
386
462
|
// ============================================================================
|
|
@@ -443,15 +519,21 @@ pub fn build_business_data_from_csv(
|
|
|
443
519
|
csv_data: String,
|
|
444
520
|
graph_json: String,
|
|
445
521
|
collections_json: String,
|
|
522
|
+
default_language: Option<String>,
|
|
523
|
+
strict_concepts: Option<bool>,
|
|
446
524
|
) -> Result<serde_json::Value> {
|
|
447
525
|
let graph: StaticGraph = serde_json::from_str(&graph_json)
|
|
448
526
|
.map_err(|e| napi::Error::from_reason(format!("Invalid graph JSON: {e}")))?;
|
|
449
527
|
let collections: Vec<SkosCollection> = serde_json::from_str(&collections_json)
|
|
450
528
|
.map_err(|e| napi::Error::from_reason(format!("Invalid collections JSON: {e}")))?;
|
|
451
529
|
|
|
452
|
-
let
|
|
453
|
-
|
|
454
|
-
|
|
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)?;
|
|
455
537
|
|
|
456
538
|
Ok(wrap_business_data(&resources))
|
|
457
539
|
}
|
|
@@ -529,6 +611,142 @@ pub fn get_registered_extension_handlers() -> Vec<String> {
|
|
|
529
611
|
.collect()
|
|
530
612
|
}
|
|
531
613
|
|
|
614
|
+
// ============================================================================
|
|
615
|
+
// SKOS parsing
|
|
616
|
+
// ============================================================================
|
|
617
|
+
|
|
618
|
+
/// Parse SKOS RDF/XML and return all collections as a JSON array.
|
|
619
|
+
#[napi(js_name = "parseSkosXml")]
|
|
620
|
+
pub fn parse_skos_xml(xml_content: String, base_uri: String) -> Result<serde_json::Value> {
|
|
621
|
+
let collections = alizarin_core::skos::parse_skos_to_collections(&xml_content, &base_uri)
|
|
622
|
+
.map_err(napi::Error::from_reason)?;
|
|
623
|
+
serde_json::to_value(&collections).map_err(|e| napi::Error::from_reason(e.to_string()))
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/// Parse SKOS RDF/XML and return a single collection (the first one found).
|
|
627
|
+
#[napi(js_name = "parseSkosXmlToCollection")]
|
|
628
|
+
pub fn parse_skos_xml_to_collection(
|
|
629
|
+
xml_content: String,
|
|
630
|
+
base_uri: String,
|
|
631
|
+
) -> Result<serde_json::Value> {
|
|
632
|
+
let mut collections = alizarin_core::skos::parse_skos_to_collections(&xml_content, &base_uri)
|
|
633
|
+
.map_err(napi::Error::from_reason)?;
|
|
634
|
+
if collections.is_empty() {
|
|
635
|
+
return Err(napi::Error::from_reason(
|
|
636
|
+
"No SKOS ConceptScheme found in XML".to_string(),
|
|
637
|
+
));
|
|
638
|
+
}
|
|
639
|
+
let collection = collections.remove(0);
|
|
640
|
+
serde_json::to_value(&collection).map_err(|e| napi::Error::from_reason(e.to_string()))
|
|
641
|
+
}
|
|
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
|
+
|
|
674
|
+
// ============================================================================
|
|
675
|
+
// Label resolution utilities
|
|
676
|
+
// ============================================================================
|
|
677
|
+
|
|
678
|
+
/// Build a mapping from node alias to collection ID based on graph definition.
|
|
679
|
+
///
|
|
680
|
+
/// Equivalent to WASM's `buildAliasToCollectionMap`.
|
|
681
|
+
#[napi(js_name = "buildAliasToCollectionMap")]
|
|
682
|
+
pub fn build_alias_to_collection_map(
|
|
683
|
+
graph_json: String,
|
|
684
|
+
resolvable_datatypes: Option<Vec<String>>,
|
|
685
|
+
config_keys: Option<Vec<String>>,
|
|
686
|
+
) -> Result<HashMap<String, String>> {
|
|
687
|
+
let graph: serde_json::Value = serde_json::from_str(&graph_json)
|
|
688
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid graph JSON: {e}")))?;
|
|
689
|
+
|
|
690
|
+
let config = alizarin_core::LabelResolutionConfig {
|
|
691
|
+
resolvable_datatypes: resolvable_datatypes.unwrap_or_else(|| {
|
|
692
|
+
alizarin_core::DEFAULT_RESOLVABLE_DATATYPES
|
|
693
|
+
.iter()
|
|
694
|
+
.map(|s| s.to_string())
|
|
695
|
+
.collect()
|
|
696
|
+
}),
|
|
697
|
+
config_keys: config_keys.unwrap_or_else(|| {
|
|
698
|
+
alizarin_core::DEFAULT_CONFIG_KEYS
|
|
699
|
+
.iter()
|
|
700
|
+
.map(|s| s.to_string())
|
|
701
|
+
.collect()
|
|
702
|
+
}),
|
|
703
|
+
strict: false,
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
Ok(alizarin_core::build_alias_to_collection_map(
|
|
707
|
+
&graph, &config,
|
|
708
|
+
))
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/// Scan a JSON tree to find which collections are needed for resolution.
|
|
712
|
+
///
|
|
713
|
+
/// Equivalent to WASM's `findNeededCollections`.
|
|
714
|
+
#[napi(js_name = "findNeededCollections")]
|
|
715
|
+
pub fn find_needed_collections(
|
|
716
|
+
tree_json: String,
|
|
717
|
+
alias_to_collection: HashMap<String, String>,
|
|
718
|
+
) -> Result<Vec<String>> {
|
|
719
|
+
let tree: serde_json::Value = serde_json::from_str(&tree_json)
|
|
720
|
+
.map_err(|e| napi::Error::from_reason(format!("Invalid tree JSON: {e}")))?;
|
|
721
|
+
|
|
722
|
+
let needed = alizarin_core::find_needed_collections(&tree, &alias_to_collection);
|
|
723
|
+
Ok(needed.into_iter().collect())
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/// Check if a string is a valid UUID.
|
|
727
|
+
#[napi(js_name = "isValidUuid")]
|
|
728
|
+
pub fn is_valid_uuid(s: String) -> bool {
|
|
729
|
+
alizarin_core::is_valid_uuid(&s)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/// Get the default resolvable datatypes (concept, concept-list).
|
|
733
|
+
#[napi(js_name = "getDefaultResolvableDatatypes")]
|
|
734
|
+
pub fn get_default_resolvable_datatypes() -> Vec<String> {
|
|
735
|
+
alizarin_core::DEFAULT_RESOLVABLE_DATATYPES
|
|
736
|
+
.iter()
|
|
737
|
+
.map(|s| s.to_string())
|
|
738
|
+
.collect()
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/// Get the default config keys for collection IDs.
|
|
742
|
+
#[napi(js_name = "getDefaultConfigKeys")]
|
|
743
|
+
pub fn get_default_config_keys() -> Vec<String> {
|
|
744
|
+
alizarin_core::DEFAULT_CONFIG_KEYS
|
|
745
|
+
.iter()
|
|
746
|
+
.map(|s| s.to_string())
|
|
747
|
+
.collect()
|
|
748
|
+
}
|
|
749
|
+
|
|
532
750
|
// ============================================================================
|
|
533
751
|
// Prebuild import (high-level convenience)
|
|
534
752
|
// ============================================================================
|