@alizarin/napi 0.2.1-alpha.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +21 -0
- package/__test__/index.mjs +533 -0
- package/alizarin-napi.linux-x64-gnu.node +0 -0
- package/bin/csv-to-prebuild.mjs +143 -0
- package/build.rs +5 -0
- package/index.d.ts +329 -0
- package/index.js +334 -0
- package/package.json +32 -0
- package/src/instance_wrapper_napi.rs +1699 -0
- package/src/lib.rs +611 -0
package/Cargo.toml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "alizarin-napi"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
authors.workspace = true
|
|
5
|
+
edition.workspace = true
|
|
6
|
+
license = "AGPL-3.0-or-later"
|
|
7
|
+
|
|
8
|
+
[lib]
|
|
9
|
+
crate-type = ["cdylib"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
alizarin-core = { path = "../alizarin-core", features = ["multi-threaded"] }
|
|
13
|
+
alizarin-clm-core = { path = "../alizarin-clm-core" }
|
|
14
|
+
alizarin-filelist-core = { path = "../alizarin-filelist-core" }
|
|
15
|
+
napi = { version = "2", features = ["napi6", "serde-json"] }
|
|
16
|
+
napi-derive = "2"
|
|
17
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
18
|
+
serde_json = "1.0"
|
|
19
|
+
|
|
20
|
+
[build-dependencies]
|
|
21
|
+
napi-build = "2"
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { NapiPrebuildLoader, NapiStaticGraph, NapiStaticResourceRegistry, NapiResourceModelWrapper, NapiResourceInstanceWrapper } from '../index.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const TEST_DATA = path.resolve(__dirname, '..', '..', '..', 'tests');
|
|
11
|
+
|
|
12
|
+
describe('NapiStaticGraph', () => {
|
|
13
|
+
it('parses a graph from JSON string', () => {
|
|
14
|
+
const graphJson = fs.readFileSync(
|
|
15
|
+
path.join(TEST_DATA, 'data', 'models', 'Person.json'),
|
|
16
|
+
'utf-8'
|
|
17
|
+
);
|
|
18
|
+
const graph = NapiStaticGraph.fromJsonString(graphJson);
|
|
19
|
+
assert.ok(graph.graphId, 'graph should have an ID');
|
|
20
|
+
assert.ok(graph.name, 'graph should have a name');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('throws on invalid JSON', () => {
|
|
24
|
+
assert.throws(() => NapiStaticGraph.fromJsonString('not json'), /error/i);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('NapiStaticResourceRegistry', () => {
|
|
29
|
+
it('starts empty', () => {
|
|
30
|
+
const registry = new NapiStaticResourceRegistry();
|
|
31
|
+
assert.equal(registry.length, 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('loads resources from business data JSON', () => {
|
|
35
|
+
const businessData = {
|
|
36
|
+
business_data: {
|
|
37
|
+
resources: [
|
|
38
|
+
{
|
|
39
|
+
resourceinstance: {
|
|
40
|
+
resourceinstanceid: '87654321-4321-4321-4321-cba987654321',
|
|
41
|
+
graph_id: '12345678-1234-1234-1234-123456789abc',
|
|
42
|
+
name: 'Test Resource',
|
|
43
|
+
descriptors: {},
|
|
44
|
+
},
|
|
45
|
+
tiles: [],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const registry = new NapiStaticResourceRegistry();
|
|
52
|
+
registry.mergeFromBusinessDataJson(JSON.stringify(businessData), true);
|
|
53
|
+
assert.ok(registry.length > 0, 'registry should have resources after merge');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('contains returns false for unknown IDs', () => {
|
|
57
|
+
const registry = new NapiStaticResourceRegistry();
|
|
58
|
+
assert.equal(registry.contains('nonexistent'), false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('NapiPrebuildLoader', () => {
|
|
63
|
+
it('throws for nonexistent directory', () => {
|
|
64
|
+
assert.throws(
|
|
65
|
+
() => new NapiPrebuildLoader('/nonexistent/path'),
|
|
66
|
+
/not found/i
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// NapiResourceModelWrapper
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
describe('NapiResourceModelWrapper', () => {
|
|
76
|
+
// Load Group.json graph for tests
|
|
77
|
+
const groupJson = fs.readFileSync(
|
|
78
|
+
path.join(TEST_DATA, 'data', 'models', 'Group.json'),
|
|
79
|
+
'utf-8'
|
|
80
|
+
);
|
|
81
|
+
const graphData = JSON.parse(groupJson);
|
|
82
|
+
const graphStr = JSON.stringify(graphData.graph[0]);
|
|
83
|
+
|
|
84
|
+
// -------------------------------------------------------------------------
|
|
85
|
+
// Construction
|
|
86
|
+
// -------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
it('constructs from graph JSON string', () => {
|
|
89
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
90
|
+
assert.ok(wrapper, 'wrapper should be defined');
|
|
91
|
+
assert.equal(wrapper.getGraphId(), '07883c9e-b25c-11e9-975a-a4d18cec433a');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('constructs from NapiStaticGraph via fromGraph()', () => {
|
|
95
|
+
const graph = NapiStaticGraph.fromJsonString(groupJson);
|
|
96
|
+
const wrapper = NapiResourceModelWrapper.fromGraph(graph, true);
|
|
97
|
+
assert.equal(wrapper.getGraphId(), '07883c9e-b25c-11e9-975a-a4d18cec433a');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('throws on invalid JSON', () => {
|
|
101
|
+
assert.throws(
|
|
102
|
+
() => new NapiResourceModelWrapper('not json', true),
|
|
103
|
+
/Invalid graph JSON/i
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
// Node accessors
|
|
109
|
+
// -------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
it('getNodeObjects returns object with all nodes', () => {
|
|
112
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
113
|
+
const nodes = wrapper.getNodeObjects();
|
|
114
|
+
assert.ok(typeof nodes === 'object');
|
|
115
|
+
assert.ok(Object.keys(nodes).length > 0, 'should have nodes');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('getNodeObjectsByAlias returns object keyed by alias', () => {
|
|
119
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
120
|
+
const byAlias = wrapper.getNodeObjectsByAlias();
|
|
121
|
+
assert.ok(byAlias['group'], 'should have "group" alias');
|
|
122
|
+
assert.ok(byAlias['name'], 'should have "name" alias');
|
|
123
|
+
assert.ok(byAlias['basic_info'], 'should have "basic_info" alias');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('getRootNode returns root', () => {
|
|
127
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
128
|
+
const root = wrapper.getRootNode();
|
|
129
|
+
assert.equal(root.alias, 'group');
|
|
130
|
+
assert.equal(root.istopnode, true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('getChildNodes returns children keyed by alias', () => {
|
|
134
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
135
|
+
const root = wrapper.getRootNode();
|
|
136
|
+
const children = wrapper.getChildNodes(root.nodeid);
|
|
137
|
+
assert.ok(typeof children === 'object');
|
|
138
|
+
assert.ok(Object.keys(children).length > 0, 'root should have children');
|
|
139
|
+
assert.ok(children['basic_info'], 'root should have basic_info child');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('getChildNodeAliases returns string array', () => {
|
|
143
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
144
|
+
const root = wrapper.getRootNode();
|
|
145
|
+
const aliases = wrapper.getChildNodeAliases(root.nodeid);
|
|
146
|
+
assert.ok(Array.isArray(aliases));
|
|
147
|
+
assert.ok(aliases.length > 0);
|
|
148
|
+
assert.ok(aliases.includes('basic_info'));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('getNodeObjectFromAlias returns correct node', () => {
|
|
152
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
153
|
+
const node = wrapper.getNodeObjectFromAlias('name');
|
|
154
|
+
assert.equal(node.alias, 'name');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('getNodeObjectFromId returns correct node', () => {
|
|
158
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
159
|
+
const root = wrapper.getRootNode();
|
|
160
|
+
const node = wrapper.getNodeObjectFromId(root.nodeid);
|
|
161
|
+
assert.equal(node.nodeid, root.nodeid);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('getNodeIdFromAlias returns correct ID', () => {
|
|
165
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
166
|
+
const nodeId = wrapper.getNodeIdFromAlias('name');
|
|
167
|
+
const node = wrapper.getNodeObjectFromId(nodeId);
|
|
168
|
+
assert.equal(node.alias, 'name');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
// Edge accessors
|
|
173
|
+
// -------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
it('getEdges returns edge map', () => {
|
|
176
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
177
|
+
const edges = wrapper.getEdges();
|
|
178
|
+
assert.ok(typeof edges === 'object');
|
|
179
|
+
const root = wrapper.getRootNode();
|
|
180
|
+
assert.ok(edges[root.nodeid], 'root should have edges');
|
|
181
|
+
assert.ok(Array.isArray(edges[root.nodeid]));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// -------------------------------------------------------------------------
|
|
185
|
+
// Nodegroup accessors
|
|
186
|
+
// -------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
it('getNodegroupObjects returns all nodegroups', () => {
|
|
189
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
190
|
+
const nodegroups = wrapper.getNodegroupObjects();
|
|
191
|
+
assert.ok(typeof nodegroups === 'object');
|
|
192
|
+
assert.ok(Object.keys(nodegroups).length > 0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('getNodegroupIds returns string array', () => {
|
|
196
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
197
|
+
const ids = wrapper.getNodegroupIds();
|
|
198
|
+
assert.ok(Array.isArray(ids));
|
|
199
|
+
assert.ok(ids.length > 0);
|
|
200
|
+
ids.forEach(id => assert.equal(typeof id, 'string'));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('getNodegroupName returns name string', () => {
|
|
204
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
205
|
+
const nameNode = wrapper.getNodeObjectFromAlias('name');
|
|
206
|
+
const name = wrapper.getNodegroupName(nameNode.nodegroup_id);
|
|
207
|
+
assert.equal(typeof name, 'string');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// -------------------------------------------------------------------------
|
|
211
|
+
// Permissions
|
|
212
|
+
// -------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
it('setPermittedNodegroups accepts boolean values', () => {
|
|
215
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
216
|
+
const ids = wrapper.getNodegroupIds();
|
|
217
|
+
const permissions = {};
|
|
218
|
+
ids.forEach(id => { permissions[id] = true; });
|
|
219
|
+
|
|
220
|
+
// Should not throw
|
|
221
|
+
wrapper.setPermittedNodegroups(permissions);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('isNodegroupPermitted returns correct boolean', () => {
|
|
225
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
226
|
+
const ids = wrapper.getNodegroupIds();
|
|
227
|
+
const permissions = {};
|
|
228
|
+
permissions[ids[0]] = true;
|
|
229
|
+
if (ids.length > 1) permissions[ids[1]] = false;
|
|
230
|
+
wrapper.setPermittedNodegroups(permissions);
|
|
231
|
+
|
|
232
|
+
assert.equal(wrapper.isNodegroupPermitted(ids[0]), true);
|
|
233
|
+
if (ids.length > 1) {
|
|
234
|
+
assert.equal(wrapper.isNodegroupPermitted(ids[1]), false);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('getPermittedNodegroups returns boolean map', () => {
|
|
239
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
240
|
+
const ids = wrapper.getNodegroupIds();
|
|
241
|
+
const permissions = {};
|
|
242
|
+
ids.forEach(id => { permissions[id] = true; });
|
|
243
|
+
wrapper.setPermittedNodegroups(permissions);
|
|
244
|
+
|
|
245
|
+
const result = wrapper.getPermittedNodegroups();
|
|
246
|
+
assert.ok(typeof result === 'object');
|
|
247
|
+
assert.ok(Object.values(result).every(v => typeof v === 'boolean'));
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('setPermittedNodegroups accepts conditional rules', () => {
|
|
251
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
252
|
+
const ids = wrapper.getNodegroupIds();
|
|
253
|
+
const permissions = {};
|
|
254
|
+
permissions[ids[0]] = { path: '.data.some_field', allowed: ['value1', 'value2'] };
|
|
255
|
+
if (ids.length > 1) permissions[ids[1]] = true;
|
|
256
|
+
|
|
257
|
+
// Should not throw
|
|
258
|
+
wrapper.setPermittedNodegroups(permissions);
|
|
259
|
+
// Conditional rule permits the nodegroup at the nodegroup level
|
|
260
|
+
assert.equal(wrapper.isNodegroupPermitted(ids[0]), true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// -------------------------------------------------------------------------
|
|
264
|
+
// Property getters
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
it('nodes property returns object', () => {
|
|
268
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
269
|
+
assert.ok(typeof wrapper.nodes === 'object');
|
|
270
|
+
assert.ok(Object.keys(wrapper.nodes).length > 0);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('edges property returns object', () => {
|
|
274
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
275
|
+
assert.ok(typeof wrapper.edges === 'object');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('nodesByAlias property returns object', () => {
|
|
279
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
280
|
+
assert.ok(typeof wrapper.nodesByAlias === 'object');
|
|
281
|
+
assert.ok(wrapper.nodesByAlias['group']);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('nodegroups property returns object', () => {
|
|
285
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
286
|
+
assert.ok(typeof wrapper.nodegroups === 'object');
|
|
287
|
+
assert.ok(Object.keys(wrapper.nodegroups).length > 0);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('graphId getter matches input', () => {
|
|
291
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
292
|
+
assert.equal(wrapper.getGraphId(), '07883c9e-b25c-11e9-975a-a4d18cec433a');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// -------------------------------------------------------------------------
|
|
296
|
+
// Graph pruning
|
|
297
|
+
// -------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
it('pruneGraph removes unpermitted nodegroups', () => {
|
|
300
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, false);
|
|
301
|
+
const ids = wrapper.getNodegroupIds();
|
|
302
|
+
|
|
303
|
+
// Permit only the first nodegroup
|
|
304
|
+
const permissions = {};
|
|
305
|
+
permissions[ids[0]] = true;
|
|
306
|
+
for (let i = 1; i < ids.length; i++) {
|
|
307
|
+
permissions[ids[i]] = false;
|
|
308
|
+
}
|
|
309
|
+
wrapper.setPermittedNodegroups(permissions);
|
|
310
|
+
|
|
311
|
+
const beforeCount = Object.keys(wrapper.nodes).length;
|
|
312
|
+
wrapper.pruneGraph();
|
|
313
|
+
const afterCount = Object.keys(wrapper.nodes).length;
|
|
314
|
+
|
|
315
|
+
assert.ok(afterCount <= beforeCount, 'pruning should reduce or maintain node count');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// -------------------------------------------------------------------------
|
|
319
|
+
// buildNodes (no-op) and buildNodesForGraph
|
|
320
|
+
// -------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
it('buildNodes is a no-op (does not throw)', () => {
|
|
323
|
+
const wrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
324
|
+
// Should not throw
|
|
325
|
+
wrapper.buildNodes();
|
|
326
|
+
assert.ok(Object.keys(wrapper.nodes).length > 0, 'nodes still accessible after buildNodes');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// =============================================================================
|
|
331
|
+
// NapiPseudoValue property names
|
|
332
|
+
// Regression: NAPI once exposed tileDataJson instead of tileData,
|
|
333
|
+
// and was missing the .node getter.
|
|
334
|
+
// =============================================================================
|
|
335
|
+
|
|
336
|
+
describe('NapiPseudoValue property names', () => {
|
|
337
|
+
// Register the graph and load a resource with tiles
|
|
338
|
+
const groupJson = fs.readFileSync(
|
|
339
|
+
path.join(TEST_DATA, 'data', 'models', 'Group.json'),
|
|
340
|
+
'utf-8'
|
|
341
|
+
);
|
|
342
|
+
const graphData = JSON.parse(groupJson);
|
|
343
|
+
const graphStr = JSON.stringify(graphData.graph[0]);
|
|
344
|
+
|
|
345
|
+
const resourceJson = fs.readFileSync(
|
|
346
|
+
path.join(TEST_DATA, 'definitions', 'resources', '_07883c9e-b25c-11e9-975a-a4d18cec433a.json'),
|
|
347
|
+
'utf-8'
|
|
348
|
+
);
|
|
349
|
+
const resourceData = JSON.parse(resourceJson);
|
|
350
|
+
const resource = resourceData.business_data.resources[0];
|
|
351
|
+
|
|
352
|
+
// Create model wrapper (registers graph)
|
|
353
|
+
const _modelWrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
354
|
+
|
|
355
|
+
// Create instance wrapper and load tiles
|
|
356
|
+
const instanceWrapper = new NapiResourceInstanceWrapper(resource.resourceinstance.graph_id);
|
|
357
|
+
instanceWrapper.loadTilesFromResource(resource);
|
|
358
|
+
|
|
359
|
+
// Get a PseudoList and PseudoValue
|
|
360
|
+
const pseudoList = instanceWrapper.getValuesAtPath('basic_info.name');
|
|
361
|
+
const pseudoValue = pseudoList.getValue(0);
|
|
362
|
+
|
|
363
|
+
it('exposes tileData (not tileDataJson)', () => {
|
|
364
|
+
assert.ok(pseudoValue, 'pseudoValue should exist');
|
|
365
|
+
// Must be .tileData, not .tileDataJson
|
|
366
|
+
assert.notEqual(pseudoValue.tileData, undefined, 'tileData should be defined');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('exposes tileId', () => {
|
|
370
|
+
const tileId = pseudoValue.tileId;
|
|
371
|
+
if (tileId != null) {
|
|
372
|
+
assert.equal(typeof tileId, 'string');
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('exposes nodeId', () => {
|
|
377
|
+
assert.equal(typeof pseudoValue.nodeId, 'string');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('exposes nodeAlias', () => {
|
|
381
|
+
if (pseudoValue.nodeAlias != null) {
|
|
382
|
+
assert.equal(typeof pseudoValue.nodeAlias, 'string');
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('exposes datatype', () => {
|
|
387
|
+
assert.equal(typeof pseudoValue.datatype, 'string');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('exposes node as object with datatype', () => {
|
|
391
|
+
const node = pseudoValue.node;
|
|
392
|
+
assert.ok(node, 'node should be defined');
|
|
393
|
+
assert.equal(typeof node, 'object');
|
|
394
|
+
assert.ok(node.datatype, 'node.datatype should be defined');
|
|
395
|
+
assert.equal(typeof node.datatype, 'string');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('exposes nodegroupId', () => {
|
|
399
|
+
if (pseudoValue.nodegroupId != null) {
|
|
400
|
+
assert.equal(typeof pseudoValue.nodegroupId, 'string');
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('exposes isCollector (boolean)', () => {
|
|
405
|
+
assert.equal(typeof pseudoValue.isCollector, 'boolean');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('exposes sortorder', () => {
|
|
409
|
+
// sortorder can be number or null — just verify it doesn't throw
|
|
410
|
+
const _sortorder = pseudoValue.sortorder;
|
|
411
|
+
assert.ok(true);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('exposes valueLoaded (boolean)', () => {
|
|
415
|
+
assert.equal(typeof pseudoValue.valueLoaded, 'boolean');
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// =============================================================================
|
|
420
|
+
// NapiResourceInstanceWrapper methods
|
|
421
|
+
// Regression: NAPI was missing setTileDataForNode, tilesLoaded, setLazy.
|
|
422
|
+
// =============================================================================
|
|
423
|
+
|
|
424
|
+
describe('NapiResourceInstanceWrapper methods', () => {
|
|
425
|
+
const groupJson = fs.readFileSync(
|
|
426
|
+
path.join(TEST_DATA, 'data', 'models', 'Group.json'),
|
|
427
|
+
'utf-8'
|
|
428
|
+
);
|
|
429
|
+
const graphData = JSON.parse(groupJson);
|
|
430
|
+
const graphStr = JSON.stringify(graphData.graph[0]);
|
|
431
|
+
const graphId = graphData.graph[0].graphid;
|
|
432
|
+
|
|
433
|
+
const resourceJson = fs.readFileSync(
|
|
434
|
+
path.join(TEST_DATA, 'definitions', 'resources', '_07883c9e-b25c-11e9-975a-a4d18cec433a.json'),
|
|
435
|
+
'utf-8'
|
|
436
|
+
);
|
|
437
|
+
const resourceData = JSON.parse(resourceJson);
|
|
438
|
+
const resource = resourceData.business_data.resources[0];
|
|
439
|
+
|
|
440
|
+
// Ensure graph is registered
|
|
441
|
+
const _modelWrapper = new NapiResourceModelWrapper(graphStr, true);
|
|
442
|
+
|
|
443
|
+
it('has setTileDataForNode method', () => {
|
|
444
|
+
const wrapper = new NapiResourceInstanceWrapper(graphId);
|
|
445
|
+
wrapper.loadTilesFromResource(resource);
|
|
446
|
+
assert.equal(typeof wrapper.setTileDataForNode, 'function');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('setTileDataForNode mutates tile data', () => {
|
|
450
|
+
const wrapper = new NapiResourceInstanceWrapper(graphId);
|
|
451
|
+
wrapper.loadTilesFromResource(resource);
|
|
452
|
+
|
|
453
|
+
const tileIds = wrapper.getAllTileIds();
|
|
454
|
+
assert.ok(tileIds.length > 0, 'should have tiles');
|
|
455
|
+
const tileId = tileIds[0];
|
|
456
|
+
|
|
457
|
+
// Set data and verify it was stored
|
|
458
|
+
const result = wrapper.setTileDataForNode(tileId, 'test-node-id', [{ name: 'test.jpg' }]);
|
|
459
|
+
assert.equal(result, true);
|
|
460
|
+
|
|
461
|
+
// Read it back
|
|
462
|
+
const data = wrapper.getTileData(tileId, 'test-node-id');
|
|
463
|
+
assert.deepEqual(data, [{ name: 'test.jpg' }]);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('has tilesLoaded method', () => {
|
|
467
|
+
const wrapper = new NapiResourceInstanceWrapper(graphId);
|
|
468
|
+
assert.equal(typeof wrapper.tilesLoaded, 'function');
|
|
469
|
+
assert.equal(wrapper.tilesLoaded(), false);
|
|
470
|
+
|
|
471
|
+
wrapper.loadTilesFromResource(resource);
|
|
472
|
+
assert.equal(wrapper.tilesLoaded(), true);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('has toJson method', () => {
|
|
476
|
+
const wrapper = new NapiResourceInstanceWrapper(graphId);
|
|
477
|
+
wrapper.loadTilesFromResource(resource);
|
|
478
|
+
assert.equal(typeof wrapper.toJson, 'function');
|
|
479
|
+
// toJson requires populate() first; just verify the method exists
|
|
480
|
+
// and that calling it without populate gives a meaningful error
|
|
481
|
+
assert.throws(() => wrapper.toJson(), /populate/i);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('has setLazy method', () => {
|
|
485
|
+
const wrapper = new NapiResourceInstanceWrapper(graphId);
|
|
486
|
+
assert.equal(typeof wrapper.setLazy, 'function');
|
|
487
|
+
// Should not throw
|
|
488
|
+
wrapper.setLazy(true);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('exportTilesJson returns valid JSON array of tiles', () => {
|
|
492
|
+
const wrapper = new NapiResourceInstanceWrapper(graphId);
|
|
493
|
+
wrapper.loadTilesFromResource(resource);
|
|
494
|
+
|
|
495
|
+
const json = wrapper.exportTilesJson();
|
|
496
|
+
assert.equal(typeof json, 'string');
|
|
497
|
+
|
|
498
|
+
const tiles = JSON.parse(json);
|
|
499
|
+
assert.ok(Array.isArray(tiles), 'parsed result should be an array');
|
|
500
|
+
assert.ok(tiles.length > 0, 'should have tiles');
|
|
501
|
+
|
|
502
|
+
// Each tile should have tileid and data
|
|
503
|
+
for (const tile of tiles) {
|
|
504
|
+
assert.ok(tile.tileid || tile.tileid === null, 'tile should have tileid field');
|
|
505
|
+
assert.ok('data' in tile, 'tile should have data field');
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('exportTilesJson reflects setTileDataForNode mutations', () => {
|
|
510
|
+
const wrapper = new NapiResourceInstanceWrapper(graphId);
|
|
511
|
+
wrapper.loadTilesFromResource(resource);
|
|
512
|
+
|
|
513
|
+
const tileIds = wrapper.getAllTileIds();
|
|
514
|
+
const tileId = tileIds[0];
|
|
515
|
+
|
|
516
|
+
// Mutate a tile
|
|
517
|
+
wrapper.setTileDataForNode(tileId, 'test-node-id', [{ name: 'mutated.jpg' }]);
|
|
518
|
+
|
|
519
|
+
// Export and verify mutation is present
|
|
520
|
+
const tiles = JSON.parse(wrapper.exportTilesJson());
|
|
521
|
+
const mutatedTile = tiles.find(t => t.tileid === tileId);
|
|
522
|
+
assert.ok(mutatedTile, 'mutated tile should be in export');
|
|
523
|
+
assert.deepEqual(mutatedTile.data['test-node-id'], [{ name: 'mutated.jpg' }]);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('exportTilesJson returns empty array when no tiles loaded', () => {
|
|
527
|
+
const wrapper = new NapiResourceInstanceWrapper(graphId);
|
|
528
|
+
const json = wrapper.exportTilesJson();
|
|
529
|
+
const tiles = JSON.parse(json);
|
|
530
|
+
assert.ok(Array.isArray(tiles), 'should be an array');
|
|
531
|
+
assert.equal(tiles.length, 0, 'should be empty');
|
|
532
|
+
});
|
|
533
|
+
});
|
|
Binary file
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* global process, console */
|
|
3
|
+
|
|
4
|
+
// Convert a CSV-based model directory into PrebuildLoader-compatible JSON.
|
|
5
|
+
//
|
|
6
|
+
// Input layout (per model):
|
|
7
|
+
// <dir>/graphs/resource_models/<model>/graph.csv
|
|
8
|
+
// <dir>/graphs/resource_models/<model>/nodes.csv
|
|
9
|
+
// <dir>/graphs/resource_models/<model>/collections.csv (optional)
|
|
10
|
+
// <dir>/business_data/<model>.csv (optional)
|
|
11
|
+
//
|
|
12
|
+
// Output layout (PrebuildLoader-compatible):
|
|
13
|
+
// <output>/graphs/resource_models/<graph_id>.json
|
|
14
|
+
// <output>/business_data/<graph_id>.json
|
|
15
|
+
//
|
|
16
|
+
// Usage:
|
|
17
|
+
// node csv-to-prebuild.mjs <input_dir> [output_dir]
|
|
18
|
+
//
|
|
19
|
+
// If output_dir is omitted, writes alongside the input (in-place conversion).
|
|
20
|
+
|
|
21
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, statSync } from 'fs';
|
|
22
|
+
import { join, basename, resolve } from 'path';
|
|
23
|
+
|
|
24
|
+
// Import from the parent package (works when run from the alizarin-napi directory
|
|
25
|
+
// or when installed as a dependency).
|
|
26
|
+
let napi;
|
|
27
|
+
try {
|
|
28
|
+
napi = await import('../index.js');
|
|
29
|
+
} catch {
|
|
30
|
+
// Fallback for when run via npx or as an installed package
|
|
31
|
+
napi = await import('@alizarin/napi');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { buildGraphFromCsvs, buildBusinessDataFromCsv } = napi;
|
|
35
|
+
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
if (args.length < 1) {
|
|
38
|
+
console.error('Usage: csv-to-prebuild <input_dir> [output_dir]');
|
|
39
|
+
console.error('');
|
|
40
|
+
console.error('Converts CSV model definitions to PrebuildLoader-compatible JSON.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const inputDir = resolve(args[0]);
|
|
45
|
+
const outputDir = resolve(args[1] || inputDir);
|
|
46
|
+
|
|
47
|
+
const modelsDir = join(inputDir, 'graphs', 'resource_models');
|
|
48
|
+
const businessDataDir = join(inputDir, 'business_data');
|
|
49
|
+
|
|
50
|
+
if (!existsSync(modelsDir)) {
|
|
51
|
+
console.error(`No graphs/resource_models/ directory found in ${inputDir}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create output directories
|
|
56
|
+
const outGraphsDir = join(outputDir, 'graphs', 'resource_models');
|
|
57
|
+
const outBusinessDir = join(outputDir, 'business_data');
|
|
58
|
+
mkdirSync(outGraphsDir, { recursive: true });
|
|
59
|
+
mkdirSync(outBusinessDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
// Find all model directories (each has graph.csv + nodes.csv)
|
|
62
|
+
const modelDirs = readdirSync(modelsDir)
|
|
63
|
+
.map(name => join(modelsDir, name))
|
|
64
|
+
.filter(p => statSync(p).isDirectory())
|
|
65
|
+
.filter(p => existsSync(join(p, 'graph.csv')) && existsSync(join(p, 'nodes.csv')));
|
|
66
|
+
|
|
67
|
+
if (modelDirs.length === 0) {
|
|
68
|
+
console.error('No model directories found with graph.csv + nodes.csv');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`Found ${modelDirs.length} model(s) in ${modelsDir}`);
|
|
73
|
+
|
|
74
|
+
const RDM_NAMESPACE = 'https://example.org/rdm/';
|
|
75
|
+
|
|
76
|
+
for (const modelDir of modelDirs) {
|
|
77
|
+
const modelName = basename(modelDir);
|
|
78
|
+
console.log(` ${modelName}:`);
|
|
79
|
+
|
|
80
|
+
// Read CSVs
|
|
81
|
+
const graphCsv = readFileSync(join(modelDir, 'graph.csv'), 'utf-8');
|
|
82
|
+
const nodesCsv = readFileSync(join(modelDir, 'nodes.csv'), 'utf-8');
|
|
83
|
+
const collectionsPath = join(modelDir, 'collections.csv');
|
|
84
|
+
const collectionsCsv = existsSync(collectionsPath)
|
|
85
|
+
? readFileSync(collectionsPath, 'utf-8')
|
|
86
|
+
: undefined;
|
|
87
|
+
|
|
88
|
+
// Build graph
|
|
89
|
+
let result;
|
|
90
|
+
try {
|
|
91
|
+
result = buildGraphFromCsvs(graphCsv, nodesCsv, collectionsCsv, RDM_NAMESPACE);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error(` Failed to build graph: ${e.message}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { graph, collections } = result;
|
|
98
|
+
const graphId = graph.graphid;
|
|
99
|
+
console.log(` graph: ${graphId}`);
|
|
100
|
+
|
|
101
|
+
// Write graph JSON
|
|
102
|
+
const graphOutPath = join(outGraphsDir, `${graphId}.json`);
|
|
103
|
+
writeFileSync(graphOutPath, JSON.stringify(graph, null, 2));
|
|
104
|
+
console.log(` wrote ${graphOutPath}`);
|
|
105
|
+
|
|
106
|
+
// Write collection files for Sparnatural concept extraction.
|
|
107
|
+
// build_sparnatural_assets.py reads from reference_data/collections/{id}.json
|
|
108
|
+
if (Array.isArray(collections) && collections.length > 0) {
|
|
109
|
+
const colDir = join(outputDir, 'reference_data', 'collections');
|
|
110
|
+
mkdirSync(colDir, { recursive: true });
|
|
111
|
+
for (const col of collections) {
|
|
112
|
+
const colId = col.id;
|
|
113
|
+
if (!colId) continue;
|
|
114
|
+
const colPath = join(colDir, `${colId}.json`);
|
|
115
|
+
writeFileSync(colPath, JSON.stringify(col, null, 2));
|
|
116
|
+
console.log(` collection: ${colId} -> ${colPath}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Look for matching business data CSV
|
|
121
|
+
// Try: business_data/<modelName>.csv
|
|
122
|
+
const bdCsvPath = join(businessDataDir, `${modelName}.csv`);
|
|
123
|
+
if (existsSync(bdCsvPath)) {
|
|
124
|
+
const csvData = readFileSync(bdCsvPath, 'utf-8');
|
|
125
|
+
try {
|
|
126
|
+
const businessData = buildBusinessDataFromCsv(
|
|
127
|
+
csvData,
|
|
128
|
+
JSON.stringify(graph),
|
|
129
|
+
JSON.stringify(collections),
|
|
130
|
+
);
|
|
131
|
+
const bdOutPath = join(outBusinessDir, `${graphId}.json`);
|
|
132
|
+
writeFileSync(bdOutPath, JSON.stringify(businessData, null, 2));
|
|
133
|
+
const resourceCount = businessData?.business_data?.resources?.length ?? 0;
|
|
134
|
+
console.log(` wrote ${bdOutPath} (${resourceCount} resources)`);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.error(` Failed to build business data: ${e.message}`);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
console.log(` no business data CSV found at ${bdCsvPath}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(`\nDone. Output: ${outputDir}`);
|