@semiont/graph 0.2.28-build.40
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/README.md +231 -0
- package/dist/index.d.ts +355 -0
- package/dist/index.js +2708 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2708 @@
|
|
|
1
|
+
// src/implementations/neptune.ts
|
|
2
|
+
import { getBodySource } from "@semiont/api-client";
|
|
3
|
+
import { getEntityTypes } from "@semiont/ontology";
|
|
4
|
+
import { getExactText } from "@semiont/api-client";
|
|
5
|
+
import { v4 as uuidv4 } from "uuid";
|
|
6
|
+
import { getTargetSource, getTargetSelector } from "@semiont/api-client";
|
|
7
|
+
import { getPrimaryRepresentation, getResourceId } from "@semiont/api-client";
|
|
8
|
+
var NeptuneClient;
|
|
9
|
+
var DescribeDBClustersCommand;
|
|
10
|
+
var gremlin;
|
|
11
|
+
var process2;
|
|
12
|
+
var TextP;
|
|
13
|
+
var order;
|
|
14
|
+
var cardinality;
|
|
15
|
+
var __;
|
|
16
|
+
async function loadDependencies() {
|
|
17
|
+
if (!NeptuneClient) {
|
|
18
|
+
const neptuneModule = await import("@aws-sdk/client-neptune");
|
|
19
|
+
NeptuneClient = neptuneModule.NeptuneClient;
|
|
20
|
+
DescribeDBClustersCommand = neptuneModule.DescribeDBClustersCommand;
|
|
21
|
+
}
|
|
22
|
+
if (!gremlin) {
|
|
23
|
+
gremlin = await import("gremlin");
|
|
24
|
+
process2 = gremlin.process;
|
|
25
|
+
TextP = process2.TextP;
|
|
26
|
+
order = process2.order;
|
|
27
|
+
cardinality = process2.cardinality;
|
|
28
|
+
__ = process2.statics;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function vertexToResource(vertex) {
|
|
32
|
+
const props = vertex.properties || vertex;
|
|
33
|
+
const getValue = (key, required = false) => {
|
|
34
|
+
const prop = props[key];
|
|
35
|
+
if (!prop) {
|
|
36
|
+
if (required) {
|
|
37
|
+
throw new Error(`Resource ${vertex.id || "unknown"} missing required field: ${key}`);
|
|
38
|
+
}
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(prop) && prop.length > 0) {
|
|
42
|
+
return prop[0].value !== void 0 ? prop[0].value : prop[0];
|
|
43
|
+
}
|
|
44
|
+
return prop.value !== void 0 ? prop.value : prop;
|
|
45
|
+
};
|
|
46
|
+
const id = getValue("id", true);
|
|
47
|
+
const name = getValue("name", true);
|
|
48
|
+
const entityTypesRaw = getValue("entityTypes", true);
|
|
49
|
+
const mediaType = getValue("mediaType", true);
|
|
50
|
+
const archived = getValue("archived", true);
|
|
51
|
+
const dateCreated = getValue("dateCreated", true);
|
|
52
|
+
const checksum = getValue("checksum", true);
|
|
53
|
+
const creatorRaw = getValue("creator", true);
|
|
54
|
+
const resource = {
|
|
55
|
+
"@context": "https://schema.org/",
|
|
56
|
+
"@id": id,
|
|
57
|
+
name,
|
|
58
|
+
entityTypes: JSON.parse(entityTypesRaw),
|
|
59
|
+
representations: [{
|
|
60
|
+
mediaType,
|
|
61
|
+
checksum,
|
|
62
|
+
rel: "original"
|
|
63
|
+
}],
|
|
64
|
+
archived: archived === "true" || archived === true,
|
|
65
|
+
dateCreated,
|
|
66
|
+
wasAttributedTo: typeof creatorRaw === "string" ? JSON.parse(creatorRaw) : creatorRaw,
|
|
67
|
+
creationMethod: getValue("creationMethod", true)
|
|
68
|
+
};
|
|
69
|
+
const sourceResourceId = getValue("sourceResourceId");
|
|
70
|
+
if (sourceResourceId) resource.sourceResourceId = sourceResourceId;
|
|
71
|
+
return resource;
|
|
72
|
+
}
|
|
73
|
+
function vertexToAnnotation(vertex, entityTypes = []) {
|
|
74
|
+
const props = vertex.properties || vertex;
|
|
75
|
+
const getValue = (key, required = false) => {
|
|
76
|
+
const prop = props[key];
|
|
77
|
+
if (!prop) {
|
|
78
|
+
if (required) {
|
|
79
|
+
throw new Error(`Annotation ${vertex.id || "unknown"} missing required field: ${key}`);
|
|
80
|
+
}
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(prop) && prop.length > 0) {
|
|
84
|
+
return prop[0].value !== void 0 ? prop[0].value : prop[0];
|
|
85
|
+
}
|
|
86
|
+
if (typeof prop === "object" && "value" in prop) return prop.value;
|
|
87
|
+
return prop;
|
|
88
|
+
};
|
|
89
|
+
const id = getValue("id", true);
|
|
90
|
+
const resourceId = getValue("resourceId", true);
|
|
91
|
+
const selectorRaw = getValue("selector", true);
|
|
92
|
+
const creatorRaw = getValue("creator", true);
|
|
93
|
+
const createdRaw = getValue("created", true);
|
|
94
|
+
const motivation = getValue("motivation") || "linking";
|
|
95
|
+
const creator = JSON.parse(creatorRaw);
|
|
96
|
+
const bodyArray = [];
|
|
97
|
+
for (const entityType of entityTypes) {
|
|
98
|
+
if (entityType) {
|
|
99
|
+
bodyArray.push({
|
|
100
|
+
type: "TextualBody",
|
|
101
|
+
value: entityType,
|
|
102
|
+
purpose: "tagging"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const bodySource = getValue("source");
|
|
107
|
+
if (bodySource) {
|
|
108
|
+
bodyArray.push({
|
|
109
|
+
type: "SpecificResource",
|
|
110
|
+
source: bodySource,
|
|
111
|
+
purpose: "linking"
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const annotation = {
|
|
115
|
+
"@context": "http://www.w3.org/ns/anno.jsonld",
|
|
116
|
+
"type": "Annotation",
|
|
117
|
+
id,
|
|
118
|
+
motivation,
|
|
119
|
+
target: {
|
|
120
|
+
source: resourceId,
|
|
121
|
+
selector: JSON.parse(selectorRaw)
|
|
122
|
+
},
|
|
123
|
+
body: bodyArray,
|
|
124
|
+
creator,
|
|
125
|
+
created: createdRaw
|
|
126
|
+
// ISO string from DB
|
|
127
|
+
};
|
|
128
|
+
const modified = getValue("modified");
|
|
129
|
+
if (modified) annotation.modified = modified;
|
|
130
|
+
const generatorJson = getValue("generator");
|
|
131
|
+
if (generatorJson) {
|
|
132
|
+
try {
|
|
133
|
+
annotation.generator = JSON.parse(generatorJson);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return annotation;
|
|
138
|
+
}
|
|
139
|
+
var NeptuneGraphDatabase = class {
|
|
140
|
+
connected = false;
|
|
141
|
+
neptuneEndpoint;
|
|
142
|
+
neptunePort = 8182;
|
|
143
|
+
region;
|
|
144
|
+
g;
|
|
145
|
+
// Gremlin graph traversal source
|
|
146
|
+
connection;
|
|
147
|
+
// Gremlin connection
|
|
148
|
+
// Helper method to fetch annotations with their entity types
|
|
149
|
+
async fetchAnnotationsWithEntityTypes(annotationVertices) {
|
|
150
|
+
const annotations = [];
|
|
151
|
+
for (const vertex of annotationVertices) {
|
|
152
|
+
const id = vertex.properties?.id?.[0]?.value || vertex.id;
|
|
153
|
+
const entityTypesResult = await this.g.V().hasLabel("Annotation").has("id", id).out("TAGGED_AS").hasLabel("EntityType").values("name").toList();
|
|
154
|
+
const entityTypes = entityTypesResult || [];
|
|
155
|
+
annotations.push(vertexToAnnotation(vertex, entityTypes));
|
|
156
|
+
}
|
|
157
|
+
return annotations;
|
|
158
|
+
}
|
|
159
|
+
constructor(config = {}) {
|
|
160
|
+
if (config.endpoint) this.neptuneEndpoint = config.endpoint;
|
|
161
|
+
this.neptunePort = config.port || 8182;
|
|
162
|
+
if (config.region) this.region = config.region;
|
|
163
|
+
}
|
|
164
|
+
async discoverNeptuneEndpoint() {
|
|
165
|
+
if (this.neptuneEndpoint) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!this.region) {
|
|
169
|
+
throw new Error("AWS region must be configured in environment JSON file (aws.region) for Neptune endpoint discovery");
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
await loadDependencies();
|
|
173
|
+
const client = new NeptuneClient({ region: this.region });
|
|
174
|
+
const command = new DescribeDBClustersCommand({});
|
|
175
|
+
const response = await client.send(command);
|
|
176
|
+
if (!response.DBClusters || response.DBClusters.length === 0) {
|
|
177
|
+
throw new Error("No Neptune clusters found in region " + this.region);
|
|
178
|
+
}
|
|
179
|
+
let cluster = null;
|
|
180
|
+
for (const dbCluster of response.DBClusters) {
|
|
181
|
+
const tagsCommand = new DescribeDBClustersCommand({
|
|
182
|
+
DBClusterIdentifier: dbCluster.DBClusterIdentifier
|
|
183
|
+
});
|
|
184
|
+
const clusterDetails = await client.send(tagsCommand);
|
|
185
|
+
if (clusterDetails.DBClusters && clusterDetails.DBClusters[0]) {
|
|
186
|
+
const clusterInfo = clusterDetails.DBClusters[0];
|
|
187
|
+
if (clusterInfo.DBClusterIdentifier?.includes("Semiont") || clusterInfo.DBClusterIdentifier?.includes("semiont")) {
|
|
188
|
+
cluster = clusterInfo;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!cluster) {
|
|
194
|
+
throw new Error("No Semiont Neptune cluster found in region " + this.region);
|
|
195
|
+
}
|
|
196
|
+
this.neptuneEndpoint = cluster.Endpoint;
|
|
197
|
+
this.neptunePort = cluster.Port || 8182;
|
|
198
|
+
console.log(`Discovered Neptune endpoint: ${this.neptuneEndpoint}:${this.neptunePort}`);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error("Failed to discover Neptune endpoint:", error);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async connect() {
|
|
205
|
+
await this.discoverNeptuneEndpoint();
|
|
206
|
+
try {
|
|
207
|
+
await loadDependencies();
|
|
208
|
+
const traversal2 = gremlin.process.AnonymousTraversalSource.traversal;
|
|
209
|
+
const DriverRemoteConnection2 = gremlin.driver.DriverRemoteConnection;
|
|
210
|
+
const connectionUrl = `wss://${this.neptuneEndpoint}:${this.neptunePort}/gremlin`;
|
|
211
|
+
console.log(`Connecting to Neptune at ${connectionUrl}`);
|
|
212
|
+
this.connection = new DriverRemoteConnection2(connectionUrl, {
|
|
213
|
+
authenticator: null,
|
|
214
|
+
// Neptune uses IAM authentication via task role
|
|
215
|
+
rejectUnauthorized: true,
|
|
216
|
+
traversalSource: "g"
|
|
217
|
+
});
|
|
218
|
+
this.g = traversal2().withRemote(this.connection);
|
|
219
|
+
const count = await this.g.V().limit(1).count().next();
|
|
220
|
+
console.log(`Connected to Neptune. Vertex count test: ${count.value}`);
|
|
221
|
+
this.connected = true;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error("Failed to connect to Neptune:", error);
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async disconnect() {
|
|
228
|
+
if (this.connection) {
|
|
229
|
+
try {
|
|
230
|
+
await this.connection.close();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error("Error closing Neptune connection:", error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
this.connected = false;
|
|
236
|
+
console.log("Disconnected from Neptune");
|
|
237
|
+
}
|
|
238
|
+
isConnected() {
|
|
239
|
+
return this.connected;
|
|
240
|
+
}
|
|
241
|
+
async createResource(resource) {
|
|
242
|
+
const id = getResourceId(resource);
|
|
243
|
+
const primaryRep = getPrimaryRepresentation(resource);
|
|
244
|
+
if (!primaryRep) {
|
|
245
|
+
throw new Error("Resource must have at least one representation");
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const vertex = this.g.addV("Resource").property("id", id).property("name", resource.name).property("mediaType", primaryRep.mediaType).property("archived", resource.archived || false).property("dateCreated", resource.dateCreated).property("creator", JSON.stringify(resource.wasAttributedTo)).property("creationMethod", resource.creationMethod).property("checksum", primaryRep.checksum).property("entityTypes", JSON.stringify(resource.entityTypes));
|
|
249
|
+
if (resource.sourceResourceId) {
|
|
250
|
+
vertex.property("sourceResourceId", resource.sourceResourceId);
|
|
251
|
+
}
|
|
252
|
+
await vertex.next();
|
|
253
|
+
console.log(`Created resource vertex in Neptune: ${id}`);
|
|
254
|
+
return resource;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error("Failed to create resource in Neptune:", error);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async getResource(id) {
|
|
261
|
+
try {
|
|
262
|
+
const result = await this.g.V().hasLabel("Resource").has("id", id).elementMap().next();
|
|
263
|
+
if (!result.value) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return vertexToResource(result.value);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error("Failed to get resource from Neptune:", error);
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async updateResource(id, input) {
|
|
273
|
+
if (Object.keys(input).length !== 1 || input.archived === void 0) {
|
|
274
|
+
throw new Error("Resources are immutable. Only archiving is allowed.");
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const result = await this.g.V().hasLabel("Resource").has("id", id).property("archived", input.archived).elementMap().next();
|
|
278
|
+
if (!result.value) {
|
|
279
|
+
throw new Error("Resource not found");
|
|
280
|
+
}
|
|
281
|
+
return vertexToResource(result.value);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error("Failed to update resource in Neptune:", error);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async deleteResource(id) {
|
|
288
|
+
try {
|
|
289
|
+
await this.g.V().hasLabel("Resource").has("id", id).drop().iterate();
|
|
290
|
+
console.log(`Deleted resource from Neptune: ${id}`);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error("Failed to delete resource from Neptune:", error);
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async listResources(filter) {
|
|
297
|
+
try {
|
|
298
|
+
let traversal2 = this.g.V().hasLabel("Resource");
|
|
299
|
+
if (filter.entityTypes && filter.entityTypes.length > 0) {
|
|
300
|
+
traversal2 = traversal2.filter(
|
|
301
|
+
process2.statics.or(
|
|
302
|
+
...filter.entityTypes.map(
|
|
303
|
+
(type) => process2.statics.has("entityTypes", TextP.containing(`"${type}"`))
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (filter.search) {
|
|
309
|
+
traversal2 = traversal2.has("name", TextP.containing(filter.search));
|
|
310
|
+
}
|
|
311
|
+
const totalResult = await traversal2.clone().count().next();
|
|
312
|
+
const total = totalResult.value || 0;
|
|
313
|
+
const offset = filter.offset || 0;
|
|
314
|
+
const limit = filter.limit || 20;
|
|
315
|
+
const results = await traversal2.order().by("created", order.desc).range(offset, offset + limit).elementMap().toList();
|
|
316
|
+
const resources = results.map(vertexToResource);
|
|
317
|
+
return { resources, total };
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error("Failed to list resources from Neptune:", error);
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async searchResources(query, limit = 20) {
|
|
324
|
+
try {
|
|
325
|
+
const results = await this.g.V().hasLabel("Resource").has("name", TextP.containing(query)).order().by("created", order.desc).limit(limit).elementMap().toList();
|
|
326
|
+
return results.map(vertexToResource);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error("Failed to search resources in Neptune:", error);
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async createAnnotation(input) {
|
|
333
|
+
const id = this.generateId();
|
|
334
|
+
const annotation = {
|
|
335
|
+
"@context": "http://www.w3.org/ns/anno.jsonld",
|
|
336
|
+
"type": "Annotation",
|
|
337
|
+
id,
|
|
338
|
+
motivation: input.motivation,
|
|
339
|
+
target: input.target,
|
|
340
|
+
body: input.body,
|
|
341
|
+
creator: input.creator,
|
|
342
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
343
|
+
};
|
|
344
|
+
const targetSource = getTargetSource(input.target);
|
|
345
|
+
const targetSelector = getTargetSelector(input.target);
|
|
346
|
+
const bodySource = getBodySource(input.body);
|
|
347
|
+
const entityTypes = getEntityTypes(input);
|
|
348
|
+
try {
|
|
349
|
+
const vertex = this.g.addV("Annotation").property("id", annotation.id).property("resourceId", targetSource).property("text", targetSelector ? getExactText(targetSelector) : "").property("selector", JSON.stringify(targetSelector || {})).property("type", "SpecificResource").property("motivation", annotation.motivation).property("creator", JSON.stringify(annotation.creator)).property("created", annotation.created);
|
|
350
|
+
if (bodySource) {
|
|
351
|
+
vertex.property("source", bodySource);
|
|
352
|
+
}
|
|
353
|
+
const newVertex = await vertex.next();
|
|
354
|
+
await this.g.V(newVertex.value).addE("BELONGS_TO").to(this.g.V().hasLabel("Resource").has("id", targetSource)).next();
|
|
355
|
+
if (bodySource) {
|
|
356
|
+
await this.g.V(newVertex.value).addE("REFERENCES").to(this.g.V().hasLabel("Resource").has("id", bodySource)).next();
|
|
357
|
+
}
|
|
358
|
+
for (const entityType of entityTypes) {
|
|
359
|
+
const etVertex = await this.g.V().hasLabel("EntityType").has("name", entityType).fold().coalesce(
|
|
360
|
+
__.unfold(),
|
|
361
|
+
this.g.addV("EntityType").property("name", entityType)
|
|
362
|
+
).next();
|
|
363
|
+
await this.g.V(newVertex.value).addE("TAGGED_AS").to(this.g.V(etVertex.value)).next();
|
|
364
|
+
}
|
|
365
|
+
console.log(`Created annotation vertex in Neptune: ${annotation.id}`);
|
|
366
|
+
return annotation;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error("Failed to create annotation in Neptune:", error);
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async getAnnotation(id) {
|
|
373
|
+
try {
|
|
374
|
+
const result = await this.g.V().hasLabel("Annotation").has("id", id).elementMap().next();
|
|
375
|
+
if (!result.value) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
const entityTypesResult = await this.g.V().hasLabel("Annotation").has("id", id).out("TAGGED_AS").hasLabel("EntityType").values("name").toList();
|
|
379
|
+
const entityTypes = entityTypesResult || [];
|
|
380
|
+
return vertexToAnnotation(result.value, entityTypes);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error("Failed to get annotation from Neptune:", error);
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async updateAnnotation(id, updates) {
|
|
387
|
+
try {
|
|
388
|
+
let traversal2 = this.g.V().hasLabel("Annotation").has("id", id);
|
|
389
|
+
if (updates.target !== void 0 && typeof updates.target !== "string") {
|
|
390
|
+
if (updates.target.selector !== void 0) {
|
|
391
|
+
traversal2 = traversal2.property("text", getExactText(updates.target.selector));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (updates.body !== void 0) {
|
|
395
|
+
const bodySource = getBodySource(updates.body);
|
|
396
|
+
const entityTypes2 = getEntityTypes({ body: updates.body });
|
|
397
|
+
if (bodySource) {
|
|
398
|
+
traversal2 = traversal2.property("source", bodySource);
|
|
399
|
+
}
|
|
400
|
+
if (entityTypes2.length >= 0) {
|
|
401
|
+
await this.g.V().hasLabel("Annotation").has("id", id).outE("TAGGED_AS").drop().iterate();
|
|
402
|
+
for (const entityType of entityTypes2) {
|
|
403
|
+
const etVertex = await this.g.V().hasLabel("EntityType").has("name", entityType).fold().coalesce(
|
|
404
|
+
__.unfold(),
|
|
405
|
+
this.g.addV("EntityType").property("name", entityType)
|
|
406
|
+
).next();
|
|
407
|
+
await this.g.V().hasLabel("Annotation").has("id", id).addE("TAGGED_AS").to(this.g.V(etVertex.value)).next();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (updates.modified !== void 0) {
|
|
412
|
+
traversal2 = traversal2.property("modified", updates.modified);
|
|
413
|
+
}
|
|
414
|
+
if (updates.generator !== void 0) {
|
|
415
|
+
traversal2 = traversal2.property("generator", JSON.stringify(updates.generator));
|
|
416
|
+
}
|
|
417
|
+
const result = await traversal2.elementMap().next();
|
|
418
|
+
if (!result.value) {
|
|
419
|
+
throw new Error("Annotation not found");
|
|
420
|
+
}
|
|
421
|
+
const entityTypesResult = await this.g.V().hasLabel("Annotation").has("id", id).out("TAGGED_AS").hasLabel("EntityType").values("name").toList();
|
|
422
|
+
const entityTypes = entityTypesResult || [];
|
|
423
|
+
return vertexToAnnotation(result.value, entityTypes);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error("Failed to update annotation in Neptune:", error);
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async deleteAnnotation(id) {
|
|
430
|
+
try {
|
|
431
|
+
await this.g.V().hasLabel("Annotation").has("id", id).drop().iterate();
|
|
432
|
+
console.log(`Deleted annotation from Neptune: ${id}`);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error("Failed to delete annotation from Neptune:", error);
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async listAnnotations(filter) {
|
|
439
|
+
try {
|
|
440
|
+
let traversal2 = this.g.V().hasLabel("Annotation");
|
|
441
|
+
if (filter.resourceId) {
|
|
442
|
+
traversal2 = traversal2.has("resourceId", filter.resourceId);
|
|
443
|
+
}
|
|
444
|
+
if (filter.type) {
|
|
445
|
+
const w3cType = filter.type === "highlight" ? "TextualBody" : "SpecificResource";
|
|
446
|
+
traversal2 = traversal2.has("type", w3cType);
|
|
447
|
+
}
|
|
448
|
+
const results = await traversal2.elementMap().toList();
|
|
449
|
+
const annotations = await this.fetchAnnotationsWithEntityTypes(results);
|
|
450
|
+
return { annotations, total: annotations.length };
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error("Failed to list annotations from Neptune:", error);
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async getHighlights(resourceId) {
|
|
457
|
+
try {
|
|
458
|
+
const results = await this.g.V().hasLabel("Annotation").has("resourceId", resourceId).hasNot("resolvedResourceId").elementMap().toList();
|
|
459
|
+
return await this.fetchAnnotationsWithEntityTypes(results);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
console.error("Failed to get highlights from Neptune:", error);
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async resolveReference(annotationId, source) {
|
|
466
|
+
try {
|
|
467
|
+
const targetDocResult = await this.g.V().hasLabel("Resource").has("id", source).elementMap().next();
|
|
468
|
+
const targetDoc = targetDocResult.value ? vertexToResource(targetDocResult.value) : null;
|
|
469
|
+
const traversal2 = this.g.V().hasLabel("Annotation").has("id", annotationId).property("source", source).property("resolvedResourceName", targetDoc?.name).property("resolvedAt", (/* @__PURE__ */ new Date()).toISOString());
|
|
470
|
+
const result = await traversal2.elementMap().next();
|
|
471
|
+
if (!result.value) {
|
|
472
|
+
throw new Error("Annotation not found");
|
|
473
|
+
}
|
|
474
|
+
const annVertex = await this.g.V().hasLabel("Annotation").has("id", annotationId).next();
|
|
475
|
+
await this.g.V(annVertex.value).addE("REFERENCES").to(this.g.V().hasLabel("Resource").has("id", source)).next();
|
|
476
|
+
const entityTypesResult = await this.g.V().hasLabel("Annotation").has("id", annotationId).out("TAGGED_AS").hasLabel("EntityType").values("name").toList();
|
|
477
|
+
const entityTypes = entityTypesResult || [];
|
|
478
|
+
return vertexToAnnotation(result.value, entityTypes);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
console.error("Failed to resolve reference in Neptune:", error);
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async getReferences(resourceId) {
|
|
485
|
+
try {
|
|
486
|
+
const results = await this.g.V().hasLabel("Annotation").has("resourceId", resourceId).has("resolvedResourceId").elementMap().toList();
|
|
487
|
+
return await this.fetchAnnotationsWithEntityTypes(results);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error("Failed to get references from Neptune:", error);
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async getEntityReferences(resourceId, entityTypes) {
|
|
494
|
+
try {
|
|
495
|
+
let traversal2 = this.g.V().hasLabel("Annotation").has("resourceId", resourceId).has("resolvedResourceId").has("entityTypes");
|
|
496
|
+
if (entityTypes && entityTypes.length > 0) {
|
|
497
|
+
traversal2 = traversal2.filter(
|
|
498
|
+
process2.statics.or(
|
|
499
|
+
...entityTypes.map(
|
|
500
|
+
(type) => process2.statics.has("entityTypes", TextP.containing(`"${type}"`))
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
const results = await traversal2.elementMap().toList();
|
|
506
|
+
return await this.fetchAnnotationsWithEntityTypes(results);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
console.error("Failed to get entity references from Neptune:", error);
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async getResourceAnnotations(resourceId) {
|
|
513
|
+
try {
|
|
514
|
+
const results = await this.g.V().hasLabel("Annotation").has("resourceId", resourceId).elementMap().toList();
|
|
515
|
+
return await this.fetchAnnotationsWithEntityTypes(results);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.error("Failed to get resource annotations from Neptune:", error);
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async getResourceReferencedBy(resourceUri3, _motivation) {
|
|
522
|
+
try {
|
|
523
|
+
const results = await this.g.V().hasLabel("Annotation").has("resolvedResourceId", resourceUri3).elementMap().toList();
|
|
524
|
+
return await this.fetchAnnotationsWithEntityTypes(results);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error("Failed to get resource referenced by from Neptune:", error);
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async getResourceConnections(resourceId) {
|
|
531
|
+
try {
|
|
532
|
+
const outgoingAnnotations = await this.g.V().hasLabel("Annotation").has("resourceId", resourceId).has("source").elementMap().toList();
|
|
533
|
+
const incomingAnnotations = await this.g.V().hasLabel("Annotation").has("source", resourceId).elementMap().toList();
|
|
534
|
+
const connectionsMap = /* @__PURE__ */ new Map();
|
|
535
|
+
for (const annVertex of outgoingAnnotations) {
|
|
536
|
+
const id = annVertex.properties?.id?.[0]?.value || annVertex.id;
|
|
537
|
+
const entityTypesResult = await this.g.V().hasLabel("Annotation").has("id", id).out("TAGGED_AS").hasLabel("EntityType").values("name").toList();
|
|
538
|
+
const entityTypes = entityTypesResult || [];
|
|
539
|
+
const annotation = vertexToAnnotation(annVertex, entityTypes);
|
|
540
|
+
const targetDocId = getBodySource(annotation.body);
|
|
541
|
+
if (!targetDocId) continue;
|
|
542
|
+
const targetDocResult = await this.g.V().hasLabel("Resource").has("id", targetDocId).elementMap().next();
|
|
543
|
+
if (targetDocResult.value) {
|
|
544
|
+
const targetDoc = vertexToResource(targetDocResult.value);
|
|
545
|
+
const targetDocId2 = getResourceId(targetDoc);
|
|
546
|
+
if (!targetDocId2) continue;
|
|
547
|
+
const existing = connectionsMap.get(targetDocId2);
|
|
548
|
+
if (existing) {
|
|
549
|
+
existing.annotations.push(annotation);
|
|
550
|
+
} else {
|
|
551
|
+
connectionsMap.set(targetDocId2, {
|
|
552
|
+
targetResource: targetDoc,
|
|
553
|
+
annotations: [annotation],
|
|
554
|
+
bidirectional: false
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
for (const annVertex of incomingAnnotations) {
|
|
560
|
+
const id = annVertex.properties?.id?.[0]?.value || annVertex.id;
|
|
561
|
+
const entityTypesResult = await this.g.V().hasLabel("Annotation").has("id", id).out("TAGGED_AS").hasLabel("EntityType").values("name").toList();
|
|
562
|
+
const entityTypes = entityTypesResult || [];
|
|
563
|
+
const annotation = vertexToAnnotation(annVertex, entityTypes);
|
|
564
|
+
const sourceDocId = getTargetSource(annotation.target);
|
|
565
|
+
const existing = connectionsMap.get(sourceDocId);
|
|
566
|
+
if (existing) {
|
|
567
|
+
existing.bidirectional = true;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return Array.from(connectionsMap.values());
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.error("Failed to get resource connections from Neptune:", error);
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async findPath(fromResourceId, toResourceId, maxDepth = 5) {
|
|
577
|
+
try {
|
|
578
|
+
const results = await this.g.V().hasLabel("Resource").has("id", fromResourceId).repeat(
|
|
579
|
+
process2.statics.both("REFERENCES").simplePath()
|
|
580
|
+
).times(maxDepth).emit().has("id", toResourceId).path().by(process2.statics.elementMap()).limit(10).toList();
|
|
581
|
+
const paths = [];
|
|
582
|
+
for (const pathResult of results) {
|
|
583
|
+
const resources = [];
|
|
584
|
+
for (let i = 0; i < pathResult.objects.length; i++) {
|
|
585
|
+
const element = pathResult.objects[i];
|
|
586
|
+
if (i % 2 === 0) {
|
|
587
|
+
resources.push(vertexToResource(element));
|
|
588
|
+
} else {
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
paths.push({ resources, annotations: [] });
|
|
592
|
+
}
|
|
593
|
+
return paths;
|
|
594
|
+
} catch (error) {
|
|
595
|
+
console.error("Failed to find paths in Neptune:", error);
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async getEntityTypeStats() {
|
|
600
|
+
try {
|
|
601
|
+
const results = await this.g.V().hasLabel("Resource").values("entityTypes").map((entityTypesJson) => {
|
|
602
|
+
const types = JSON.parse(entityTypesJson);
|
|
603
|
+
return types;
|
|
604
|
+
}).unfold().groupCount().next();
|
|
605
|
+
const stats = [];
|
|
606
|
+
if (results.value) {
|
|
607
|
+
for (const [type, count] of Object.entries(results.value)) {
|
|
608
|
+
stats.push({
|
|
609
|
+
type,
|
|
610
|
+
count
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return stats;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
console.error("Failed to get entity type stats from Neptune:", error);
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async getStats() {
|
|
621
|
+
try {
|
|
622
|
+
const docCountResult = await this.g.V().hasLabel("Resource").count().next();
|
|
623
|
+
const resourceCount = docCountResult.value || 0;
|
|
624
|
+
const selCountResult = await this.g.V().hasLabel("Annotation").count().next();
|
|
625
|
+
const annotationCount = selCountResult.value || 0;
|
|
626
|
+
const highlightCountResult = await this.g.V().hasLabel("Annotation").hasNot("resolvedResourceId").count().next();
|
|
627
|
+
const highlightCount = highlightCountResult.value || 0;
|
|
628
|
+
const referenceCountResult = await this.g.V().hasLabel("Annotation").has("resolvedResourceId").count().next();
|
|
629
|
+
const referenceCount = referenceCountResult.value || 0;
|
|
630
|
+
const entityRefCountResult = await this.g.V().hasLabel("Annotation").has("resolvedResourceId").has("entityTypes").count().next();
|
|
631
|
+
const entityReferenceCount = entityRefCountResult.value || 0;
|
|
632
|
+
const entityTypeStats = await this.getEntityTypeStats();
|
|
633
|
+
const entityTypes = {};
|
|
634
|
+
for (const stat of entityTypeStats) {
|
|
635
|
+
entityTypes[stat.type] = stat.count;
|
|
636
|
+
}
|
|
637
|
+
const contentTypeResult = await this.g.V().hasLabel("Resource").groupCount().by("contentType").next();
|
|
638
|
+
const contentTypes = contentTypeResult.value || {};
|
|
639
|
+
return {
|
|
640
|
+
resourceCount,
|
|
641
|
+
annotationCount,
|
|
642
|
+
highlightCount,
|
|
643
|
+
referenceCount,
|
|
644
|
+
entityReferenceCount,
|
|
645
|
+
entityTypes,
|
|
646
|
+
contentTypes
|
|
647
|
+
};
|
|
648
|
+
} catch (error) {
|
|
649
|
+
console.error("Failed to get stats from Neptune:", error);
|
|
650
|
+
throw error;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async createAnnotations(inputs) {
|
|
654
|
+
const results = [];
|
|
655
|
+
try {
|
|
656
|
+
for (const input of inputs) {
|
|
657
|
+
const annotation = await this.createAnnotation(input);
|
|
658
|
+
results.push(annotation);
|
|
659
|
+
}
|
|
660
|
+
return results;
|
|
661
|
+
} catch (error) {
|
|
662
|
+
console.error("Failed to create annotations in Neptune:", error);
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async resolveReferences(inputs) {
|
|
667
|
+
const results = [];
|
|
668
|
+
try {
|
|
669
|
+
for (const input of inputs) {
|
|
670
|
+
const annotation = await this.resolveReference(input.annotationId, input.source);
|
|
671
|
+
results.push(annotation);
|
|
672
|
+
}
|
|
673
|
+
return results;
|
|
674
|
+
} catch (error) {
|
|
675
|
+
console.error("Failed to resolve references in Neptune:", error);
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async detectAnnotations(_resourceId) {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
// Tag Collections - stored as special vertices in the graph
|
|
683
|
+
entityTypesCollection = null;
|
|
684
|
+
async getEntityTypes() {
|
|
685
|
+
if (this.entityTypesCollection === null) {
|
|
686
|
+
await this.initializeTagCollections();
|
|
687
|
+
}
|
|
688
|
+
return Array.from(this.entityTypesCollection).sort();
|
|
689
|
+
}
|
|
690
|
+
async addEntityType(tag) {
|
|
691
|
+
if (this.entityTypesCollection === null) {
|
|
692
|
+
await this.initializeTagCollections();
|
|
693
|
+
}
|
|
694
|
+
this.entityTypesCollection.add(tag);
|
|
695
|
+
try {
|
|
696
|
+
await this.g.V().has("tagCollection", "type", "entity-types").fold().coalesce(
|
|
697
|
+
__.unfold(),
|
|
698
|
+
__.addV("TagCollection").property("type", "entity-types")
|
|
699
|
+
).property(cardinality.set, "tags", tag).iterate();
|
|
700
|
+
} catch (error) {
|
|
701
|
+
console.error("Failed to add entity type:", error);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async addEntityTypes(tags) {
|
|
705
|
+
if (this.entityTypesCollection === null) {
|
|
706
|
+
await this.initializeTagCollections();
|
|
707
|
+
}
|
|
708
|
+
tags.forEach((tag) => this.entityTypesCollection.add(tag));
|
|
709
|
+
try {
|
|
710
|
+
const vertex = await this.g.V().has("tagCollection", "type", "entity-types").fold().coalesce(
|
|
711
|
+
__.unfold(),
|
|
712
|
+
__.addV("TagCollection").property("type", "entity-types")
|
|
713
|
+
);
|
|
714
|
+
for (const tag of tags) {
|
|
715
|
+
await vertex.property(cardinality.set, "tags", tag).iterate();
|
|
716
|
+
}
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.error("Failed to add entity types:", error);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async initializeTagCollections() {
|
|
722
|
+
try {
|
|
723
|
+
const collections = await this.g.V().hasLabel("TagCollection").project("type", "tags").by("type").by(__.values("tags").fold()).toList();
|
|
724
|
+
for (const col of collections) {
|
|
725
|
+
if (col.type === "entity-types") {
|
|
726
|
+
this.entityTypesCollection = new Set(col.tags);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
} catch (error) {
|
|
730
|
+
console.log("No existing tag collections found, will initialize with defaults");
|
|
731
|
+
}
|
|
732
|
+
if (this.entityTypesCollection === null) {
|
|
733
|
+
const { DEFAULT_ENTITY_TYPES } = await import("@semiont/ontology");
|
|
734
|
+
this.entityTypesCollection = new Set(DEFAULT_ENTITY_TYPES);
|
|
735
|
+
try {
|
|
736
|
+
const vertex = await this.g.addV("TagCollection").property("type", "entity-types").next();
|
|
737
|
+
for (const tag of DEFAULT_ENTITY_TYPES) {
|
|
738
|
+
await this.g.V(vertex.value.id).property(cardinality.set, "tags", tag).iterate();
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
console.error("Failed to initialize entity types:", error);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
generateId() {
|
|
746
|
+
return uuidv4().replace(/-/g, "").substring(0, 12);
|
|
747
|
+
}
|
|
748
|
+
async clearDatabase() {
|
|
749
|
+
try {
|
|
750
|
+
await this.g.V().drop().iterate();
|
|
751
|
+
console.log("Cleared all data from Neptune");
|
|
752
|
+
this.entityTypesCollection = null;
|
|
753
|
+
} catch (error) {
|
|
754
|
+
console.error("Failed to clear Neptune database:", error);
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// src/implementations/neo4j.ts
|
|
761
|
+
import neo4j from "neo4j-driver";
|
|
762
|
+
import { getExactText as getExactText2 } from "@semiont/api-client";
|
|
763
|
+
import { v4 as uuidv42 } from "uuid";
|
|
764
|
+
import { getBodySource as getBodySource2, getTargetSource as getTargetSource2, getTargetSelector as getTargetSelector2 } from "@semiont/api-client";
|
|
765
|
+
import { getEntityTypes as getEntityTypes2 } from "@semiont/ontology";
|
|
766
|
+
import { getPrimaryRepresentation as getPrimaryRepresentation2 } from "@semiont/api-client";
|
|
767
|
+
function motivationToLabel(motivation) {
|
|
768
|
+
return motivation.charAt(0).toUpperCase() + motivation.slice(1);
|
|
769
|
+
}
|
|
770
|
+
var Neo4jGraphDatabase = class {
|
|
771
|
+
driver = null;
|
|
772
|
+
connected = false;
|
|
773
|
+
config;
|
|
774
|
+
// Tag Collections - cached in memory for performance
|
|
775
|
+
entityTypesCollection = null;
|
|
776
|
+
constructor(config = {}) {
|
|
777
|
+
this.config = config;
|
|
778
|
+
}
|
|
779
|
+
async connect() {
|
|
780
|
+
try {
|
|
781
|
+
const uri = this.config.uri;
|
|
782
|
+
const username = this.config.username;
|
|
783
|
+
const password = this.config.password;
|
|
784
|
+
const database = this.config.database;
|
|
785
|
+
if (!uri) {
|
|
786
|
+
throw new Error("Neo4j URI not configured! Pass uri in config.");
|
|
787
|
+
}
|
|
788
|
+
if (!username) {
|
|
789
|
+
throw new Error("Neo4j username not configured! Pass username in config.");
|
|
790
|
+
}
|
|
791
|
+
if (!password) {
|
|
792
|
+
throw new Error("Neo4j password not configured! Pass password in config.");
|
|
793
|
+
}
|
|
794
|
+
if (!database) {
|
|
795
|
+
throw new Error("Neo4j database not configured! Pass database in config.");
|
|
796
|
+
}
|
|
797
|
+
console.log(`Connecting to Neo4j at ${uri}...`);
|
|
798
|
+
this.driver = neo4j.driver(
|
|
799
|
+
uri,
|
|
800
|
+
neo4j.auth.basic(username, password),
|
|
801
|
+
{
|
|
802
|
+
maxConnectionPoolSize: 50,
|
|
803
|
+
connectionAcquisitionTimeout: 6e4
|
|
804
|
+
}
|
|
805
|
+
);
|
|
806
|
+
const session = this.driver.session({ database });
|
|
807
|
+
await session.run("RETURN 1 as test");
|
|
808
|
+
await session.close();
|
|
809
|
+
await this.ensureSchemaExists();
|
|
810
|
+
console.log("Successfully connected to Neo4j");
|
|
811
|
+
this.connected = true;
|
|
812
|
+
} catch (error) {
|
|
813
|
+
console.error("Failed to connect to Neo4j:", error);
|
|
814
|
+
throw new Error(`Neo4j connection failed: ${error}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async disconnect() {
|
|
818
|
+
if (this.driver) {
|
|
819
|
+
await this.driver.close();
|
|
820
|
+
this.driver = null;
|
|
821
|
+
}
|
|
822
|
+
this.connected = false;
|
|
823
|
+
}
|
|
824
|
+
isConnected() {
|
|
825
|
+
return this.connected;
|
|
826
|
+
}
|
|
827
|
+
getSession() {
|
|
828
|
+
if (!this.driver) {
|
|
829
|
+
throw new Error("Neo4j driver not initialized");
|
|
830
|
+
}
|
|
831
|
+
if (!this.config.database) {
|
|
832
|
+
throw new Error("Neo4j database not configured! Pass database in config.");
|
|
833
|
+
}
|
|
834
|
+
return this.driver.session({
|
|
835
|
+
database: this.config.database
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
async ensureSchemaExists() {
|
|
839
|
+
const session = this.getSession();
|
|
840
|
+
try {
|
|
841
|
+
const constraints = [
|
|
842
|
+
"CREATE CONSTRAINT doc_id IF NOT EXISTS FOR (d:Resource) REQUIRE d.id IS UNIQUE",
|
|
843
|
+
"CREATE CONSTRAINT sel_id IF NOT EXISTS FOR (s:Annotation) REQUIRE s.id IS UNIQUE",
|
|
844
|
+
"CREATE CONSTRAINT tag_id IF NOT EXISTS FOR (t:TagCollection) REQUIRE t.type IS UNIQUE"
|
|
845
|
+
];
|
|
846
|
+
for (const constraint of constraints) {
|
|
847
|
+
try {
|
|
848
|
+
await session.run(constraint);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
if (!error.message?.includes("already exists")) {
|
|
851
|
+
console.warn(`Schema creation warning: ${error.message}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const indexes = [
|
|
856
|
+
"CREATE INDEX doc_name IF NOT EXISTS FOR (d:Resource) ON (d.name)",
|
|
857
|
+
"CREATE INDEX doc_entity_types IF NOT EXISTS FOR (d:Resource) ON (d.entityTypes)",
|
|
858
|
+
"CREATE INDEX sel_doc_id IF NOT EXISTS FOR (s:Annotation) ON (s.resourceId)",
|
|
859
|
+
"CREATE INDEX sel_resolved_id IF NOT EXISTS FOR (s:Annotation) ON (s.resolvedResourceId)"
|
|
860
|
+
];
|
|
861
|
+
for (const index of indexes) {
|
|
862
|
+
try {
|
|
863
|
+
await session.run(index);
|
|
864
|
+
} catch (error) {
|
|
865
|
+
if (!error.message?.includes("already exists")) {
|
|
866
|
+
console.warn(`Index creation warning: ${error.message}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} finally {
|
|
871
|
+
await session.close();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async createResource(resource) {
|
|
875
|
+
const session = this.getSession();
|
|
876
|
+
try {
|
|
877
|
+
const id = resource["@id"];
|
|
878
|
+
const primaryRep = getPrimaryRepresentation2(resource);
|
|
879
|
+
if (!primaryRep) {
|
|
880
|
+
throw new Error("Resource must have at least one representation");
|
|
881
|
+
}
|
|
882
|
+
const result = await session.run(
|
|
883
|
+
`CREATE (d:Resource {
|
|
884
|
+
id: $id,
|
|
885
|
+
name: $name,
|
|
886
|
+
entityTypes: $entityTypes,
|
|
887
|
+
format: $format,
|
|
888
|
+
archived: $archived,
|
|
889
|
+
created: datetime($created),
|
|
890
|
+
creator: $creator,
|
|
891
|
+
creationMethod: $creationMethod,
|
|
892
|
+
contentChecksum: $contentChecksum,
|
|
893
|
+
sourceAnnotationId: $sourceAnnotationId,
|
|
894
|
+
sourceResourceId: $sourceResourceId
|
|
895
|
+
}) RETURN d`,
|
|
896
|
+
{
|
|
897
|
+
id,
|
|
898
|
+
name: resource.name,
|
|
899
|
+
entityTypes: resource.entityTypes,
|
|
900
|
+
format: primaryRep.mediaType,
|
|
901
|
+
archived: resource.archived || false,
|
|
902
|
+
created: resource.dateCreated,
|
|
903
|
+
creator: JSON.stringify(resource.wasAttributedTo),
|
|
904
|
+
creationMethod: resource.creationMethod,
|
|
905
|
+
contentChecksum: primaryRep.checksum,
|
|
906
|
+
sourceAnnotationId: resource.sourceAnnotationId ?? null,
|
|
907
|
+
sourceResourceId: resource.sourceResourceId ?? null
|
|
908
|
+
}
|
|
909
|
+
);
|
|
910
|
+
return this.parseResourceNode(result.records[0].get("d"));
|
|
911
|
+
} finally {
|
|
912
|
+
await session.close();
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
async getResource(id) {
|
|
916
|
+
const session = this.getSession();
|
|
917
|
+
try {
|
|
918
|
+
const result = await session.run(
|
|
919
|
+
"MATCH (d:Resource {id: $id}) RETURN d",
|
|
920
|
+
{ id }
|
|
921
|
+
);
|
|
922
|
+
if (result.records.length === 0) return null;
|
|
923
|
+
return this.parseResourceNode(result.records[0].get("d"));
|
|
924
|
+
} finally {
|
|
925
|
+
await session.close();
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
async updateResource(id, input) {
|
|
929
|
+
if (Object.keys(input).length !== 1 || input.archived === void 0) {
|
|
930
|
+
throw new Error("Resources are immutable. Only archiving is allowed.");
|
|
931
|
+
}
|
|
932
|
+
const session = this.getSession();
|
|
933
|
+
try {
|
|
934
|
+
const result = await session.run(
|
|
935
|
+
`MATCH (d:Resource {id: $id})
|
|
936
|
+
SET d.archived = $archived
|
|
937
|
+
RETURN d`,
|
|
938
|
+
{ id, archived: input.archived }
|
|
939
|
+
);
|
|
940
|
+
if (result.records.length === 0) {
|
|
941
|
+
throw new Error("Resource not found");
|
|
942
|
+
}
|
|
943
|
+
return this.parseResourceNode(result.records[0].get("d"));
|
|
944
|
+
} finally {
|
|
945
|
+
await session.close();
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async deleteResource(id) {
|
|
949
|
+
const session = this.getSession();
|
|
950
|
+
try {
|
|
951
|
+
await session.run(
|
|
952
|
+
`MATCH (d:Resource {id: $id})
|
|
953
|
+
OPTIONAL MATCH (a:Annotation)-[:BELONGS_TO|:REFERENCES]->(d)
|
|
954
|
+
DETACH DELETE d, a`,
|
|
955
|
+
{ id }
|
|
956
|
+
);
|
|
957
|
+
} finally {
|
|
958
|
+
await session.close();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async listResources(filter) {
|
|
962
|
+
const session = this.getSession();
|
|
963
|
+
try {
|
|
964
|
+
let whereClause = "";
|
|
965
|
+
const params = {};
|
|
966
|
+
const conditions = [];
|
|
967
|
+
if (filter.entityTypes && filter.entityTypes.length > 0) {
|
|
968
|
+
conditions.push("ANY(type IN $entityTypes WHERE type IN d.entityTypes)");
|
|
969
|
+
params.entityTypes = filter.entityTypes;
|
|
970
|
+
}
|
|
971
|
+
if (filter.search) {
|
|
972
|
+
conditions.push("toLower(d.name) CONTAINS toLower($search)");
|
|
973
|
+
params.search = filter.search;
|
|
974
|
+
}
|
|
975
|
+
if (conditions.length > 0) {
|
|
976
|
+
whereClause = "WHERE " + conditions.join(" AND ");
|
|
977
|
+
}
|
|
978
|
+
const countResult = await session.run(
|
|
979
|
+
`MATCH (d:Resource) ${whereClause} RETURN count(d) as total`,
|
|
980
|
+
params
|
|
981
|
+
);
|
|
982
|
+
const total = countResult.records[0].get("total").toNumber();
|
|
983
|
+
params.skip = neo4j.int(filter.offset || 0);
|
|
984
|
+
params.limit = neo4j.int(filter.limit || 20);
|
|
985
|
+
const result = await session.run(
|
|
986
|
+
`MATCH (d:Resource) ${whereClause}
|
|
987
|
+
RETURN d
|
|
988
|
+
ORDER BY d.updatedAt DESC
|
|
989
|
+
SKIP $skip LIMIT $limit`,
|
|
990
|
+
params
|
|
991
|
+
);
|
|
992
|
+
const resources = result.records.map((record) => this.parseResourceNode(record.get("d")));
|
|
993
|
+
return { resources, total };
|
|
994
|
+
} finally {
|
|
995
|
+
await session.close();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async searchResources(query, limit = 20) {
|
|
999
|
+
const session = this.getSession();
|
|
1000
|
+
try {
|
|
1001
|
+
const result = await session.run(
|
|
1002
|
+
`MATCH (d:Resource)
|
|
1003
|
+
WHERE toLower(d.name) CONTAINS toLower($query)
|
|
1004
|
+
RETURN d
|
|
1005
|
+
ORDER BY d.updatedAt DESC
|
|
1006
|
+
LIMIT $limit`,
|
|
1007
|
+
{ query, limit: neo4j.int(limit) }
|
|
1008
|
+
);
|
|
1009
|
+
return result.records.map((record) => this.parseResourceNode(record.get("d")));
|
|
1010
|
+
} finally {
|
|
1011
|
+
await session.close();
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async createAnnotation(input) {
|
|
1015
|
+
const session = this.getSession();
|
|
1016
|
+
try {
|
|
1017
|
+
const id = input.id;
|
|
1018
|
+
const annotation = {
|
|
1019
|
+
"@context": "http://www.w3.org/ns/anno.jsonld",
|
|
1020
|
+
"type": "Annotation",
|
|
1021
|
+
id,
|
|
1022
|
+
motivation: input.motivation,
|
|
1023
|
+
target: input.target,
|
|
1024
|
+
body: input.body,
|
|
1025
|
+
creator: input.creator,
|
|
1026
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
1027
|
+
};
|
|
1028
|
+
const targetSource = getTargetSource2(input.target);
|
|
1029
|
+
const targetSelector = getTargetSelector2(input.target);
|
|
1030
|
+
const bodySource = getBodySource2(input.body);
|
|
1031
|
+
const entityTypes = getEntityTypes2(input);
|
|
1032
|
+
const motivationLabel = motivationToLabel(annotation.motivation);
|
|
1033
|
+
let cypher;
|
|
1034
|
+
if (bodySource) {
|
|
1035
|
+
cypher = `MATCH (from:Resource {id: $fromId})
|
|
1036
|
+
MATCH (to:Resource {id: $toId})
|
|
1037
|
+
CREATE (a:Annotation:${motivationLabel} {
|
|
1038
|
+
id: $id,
|
|
1039
|
+
resourceId: $resourceId,
|
|
1040
|
+
exact: $exact,
|
|
1041
|
+
selector: $selector,
|
|
1042
|
+
type: $type,
|
|
1043
|
+
motivation: $motivation,
|
|
1044
|
+
creator: $creator,
|
|
1045
|
+
created: datetime($created),
|
|
1046
|
+
source: $source
|
|
1047
|
+
})
|
|
1048
|
+
CREATE (a)-[:BELONGS_TO]->(from)
|
|
1049
|
+
CREATE (a)-[:REFERENCES]->(to)
|
|
1050
|
+
FOREACH (entityType IN $entityTypes |
|
|
1051
|
+
MERGE (et:EntityType {name: entityType})
|
|
1052
|
+
CREATE (a)-[:TAGGED_AS]->(et)
|
|
1053
|
+
)
|
|
1054
|
+
RETURN a`;
|
|
1055
|
+
} else {
|
|
1056
|
+
cypher = `MATCH (d:Resource {id: $resourceId})
|
|
1057
|
+
CREATE (a:Annotation:${motivationLabel} {
|
|
1058
|
+
id: $id,
|
|
1059
|
+
resourceId: $resourceId,
|
|
1060
|
+
exact: $exact,
|
|
1061
|
+
selector: $selector,
|
|
1062
|
+
type: $type,
|
|
1063
|
+
motivation: $motivation,
|
|
1064
|
+
creator: $creator,
|
|
1065
|
+
created: datetime($created)
|
|
1066
|
+
})
|
|
1067
|
+
CREATE (a)-[:BELONGS_TO]->(d)
|
|
1068
|
+
FOREACH (entityType IN $entityTypes |
|
|
1069
|
+
MERGE (et:EntityType {name: entityType})
|
|
1070
|
+
CREATE (a)-[:TAGGED_AS]->(et)
|
|
1071
|
+
)
|
|
1072
|
+
RETURN a`;
|
|
1073
|
+
}
|
|
1074
|
+
const params = {
|
|
1075
|
+
id,
|
|
1076
|
+
resourceId: targetSource,
|
|
1077
|
+
// Store full URI
|
|
1078
|
+
fromId: targetSource,
|
|
1079
|
+
// Store full URI
|
|
1080
|
+
toId: bodySource || null,
|
|
1081
|
+
// Store full URI
|
|
1082
|
+
exact: targetSelector ? getExactText2(targetSelector) : "",
|
|
1083
|
+
selector: JSON.stringify(targetSelector || {}),
|
|
1084
|
+
type: "SpecificResource",
|
|
1085
|
+
motivation: annotation.motivation,
|
|
1086
|
+
creator: JSON.stringify(annotation.creator),
|
|
1087
|
+
created: annotation.created,
|
|
1088
|
+
entityTypes,
|
|
1089
|
+
source: bodySource || null
|
|
1090
|
+
};
|
|
1091
|
+
const result = await session.run(cypher, params);
|
|
1092
|
+
if (result.records.length === 0) {
|
|
1093
|
+
throw new Error(`Failed to create annotation: Resource ${targetSource} not found in graph database`);
|
|
1094
|
+
}
|
|
1095
|
+
return this.parseAnnotationNode(result.records[0].get("a"), entityTypes);
|
|
1096
|
+
} finally {
|
|
1097
|
+
await session.close();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async getAnnotation(id) {
|
|
1101
|
+
console.log(`[Neo4j] getAnnotation called for: ${id}`);
|
|
1102
|
+
const session = this.getSession();
|
|
1103
|
+
try {
|
|
1104
|
+
const result = await session.run(
|
|
1105
|
+
`MATCH (a:Annotation {id: $id})
|
|
1106
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1107
|
+
RETURN a, collect(et.name) as entityTypes`,
|
|
1108
|
+
{ id }
|
|
1109
|
+
);
|
|
1110
|
+
if (result.records.length === 0) {
|
|
1111
|
+
console.log(`[Neo4j] getAnnotation: Annotation ${id} NOT FOUND`);
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
console.log(`[Neo4j] getAnnotation: Annotation ${id} FOUND`);
|
|
1115
|
+
return this.parseAnnotationNode(
|
|
1116
|
+
result.records[0].get("a"),
|
|
1117
|
+
result.records[0].get("entityTypes")
|
|
1118
|
+
);
|
|
1119
|
+
} finally {
|
|
1120
|
+
await session.close();
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
async updateAnnotation(id, updates) {
|
|
1124
|
+
const session = this.getSession();
|
|
1125
|
+
try {
|
|
1126
|
+
const setClauses = ["a.updatedAt = datetime()"];
|
|
1127
|
+
const params = { id };
|
|
1128
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
1129
|
+
if (key !== "id" && key !== "updatedAt") {
|
|
1130
|
+
setClauses.push(`a.${key} = $${key}`);
|
|
1131
|
+
if (key === "selector" || key === "metadata" || key === "body") {
|
|
1132
|
+
params[key] = JSON.stringify(value);
|
|
1133
|
+
} else if (key === "created" || key === "resolvedAt") {
|
|
1134
|
+
params[key] = value ? new Date(value).toISOString() : null;
|
|
1135
|
+
} else {
|
|
1136
|
+
params[key] = value;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
const result = await session.run(
|
|
1141
|
+
`MATCH (a:Annotation {id: $id})
|
|
1142
|
+
SET ${setClauses.join(", ")}
|
|
1143
|
+
WITH a
|
|
1144
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1145
|
+
RETURN a, collect(et.name) as entityTypes`,
|
|
1146
|
+
params
|
|
1147
|
+
);
|
|
1148
|
+
if (result.records.length === 0) {
|
|
1149
|
+
throw new Error("Annotation not found");
|
|
1150
|
+
}
|
|
1151
|
+
if (updates.motivation) {
|
|
1152
|
+
const newLabel = motivationToLabel(updates.motivation);
|
|
1153
|
+
console.log(`[Neo4j] Updating motivation label to: ${newLabel}`);
|
|
1154
|
+
const allMotivations = [
|
|
1155
|
+
"Assessing",
|
|
1156
|
+
"Bookmarking",
|
|
1157
|
+
"Classifying",
|
|
1158
|
+
"Commenting",
|
|
1159
|
+
"Describing",
|
|
1160
|
+
"Editing",
|
|
1161
|
+
"Highlighting",
|
|
1162
|
+
"Identifying",
|
|
1163
|
+
"Linking",
|
|
1164
|
+
"Moderating",
|
|
1165
|
+
"Questioning",
|
|
1166
|
+
"Replying",
|
|
1167
|
+
"Tagging"
|
|
1168
|
+
];
|
|
1169
|
+
const removeLabels = allMotivations.filter((m) => m !== newLabel).map((m) => `a:${m}`).join(", ");
|
|
1170
|
+
await session.run(
|
|
1171
|
+
`MATCH (a:Annotation {id: $id})
|
|
1172
|
+
REMOVE ${removeLabels}
|
|
1173
|
+
SET a:${newLabel}`,
|
|
1174
|
+
{ id }
|
|
1175
|
+
);
|
|
1176
|
+
console.log(`[Neo4j] \u2705 Motivation label updated to: ${newLabel}`);
|
|
1177
|
+
}
|
|
1178
|
+
if (updates.body) {
|
|
1179
|
+
console.log(`[Neo4j] ====== BODY UPDATE for Annotation ${id} ======`);
|
|
1180
|
+
console.log(`[Neo4j] updates.body:`, JSON.stringify(updates.body));
|
|
1181
|
+
const bodyArray = Array.isArray(updates.body) ? updates.body : [updates.body];
|
|
1182
|
+
console.log(`[Neo4j] bodyArray length: ${bodyArray.length}`);
|
|
1183
|
+
bodyArray.forEach((item, idx) => {
|
|
1184
|
+
console.log(`[Neo4j] Body item ${idx}:`, JSON.stringify(item));
|
|
1185
|
+
});
|
|
1186
|
+
const specificResource = bodyArray.find((item) => item.type === "SpecificResource" && item.purpose === "linking");
|
|
1187
|
+
console.log(`[Neo4j] Found SpecificResource:`, specificResource ? JSON.stringify(specificResource) : "null");
|
|
1188
|
+
if (specificResource && "source" in specificResource && specificResource.source) {
|
|
1189
|
+
console.log(`[Neo4j] \u2705 Creating REFERENCES edge: ${id} -> ${specificResource.source}`);
|
|
1190
|
+
const refResult = await session.run(
|
|
1191
|
+
`MATCH (a:Annotation {id: $annotationId})
|
|
1192
|
+
MATCH (target:Resource {id: $targetResourceId})
|
|
1193
|
+
MERGE (a)-[:REFERENCES]->(target)
|
|
1194
|
+
RETURN a, target`,
|
|
1195
|
+
{
|
|
1196
|
+
annotationId: id,
|
|
1197
|
+
targetResourceId: specificResource.source
|
|
1198
|
+
}
|
|
1199
|
+
);
|
|
1200
|
+
console.log(`[Neo4j] \u2705 REFERENCES edge created! Matched ${refResult.records.length} nodes`);
|
|
1201
|
+
if (refResult.records.length > 0) {
|
|
1202
|
+
console.log(`[Neo4j] Annotation: ${refResult.records[0].get("a").properties.id}`);
|
|
1203
|
+
console.log(`[Neo4j] Target Resource: ${refResult.records[0].get("target").properties.id}`);
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
console.log(`[Neo4j] No SpecificResource in body - this is a stub reference (not yet resolved)`);
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
console.log(`[Neo4j] No body update for annotation ${id}`);
|
|
1210
|
+
}
|
|
1211
|
+
return this.parseAnnotationNode(
|
|
1212
|
+
result.records[0].get("a"),
|
|
1213
|
+
result.records[0].get("entityTypes")
|
|
1214
|
+
);
|
|
1215
|
+
} finally {
|
|
1216
|
+
await session.close();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
async deleteAnnotation(id) {
|
|
1220
|
+
const session = this.getSession();
|
|
1221
|
+
try {
|
|
1222
|
+
await session.run(
|
|
1223
|
+
"MATCH (a:Annotation {id: $id}) DETACH DELETE a",
|
|
1224
|
+
{ id }
|
|
1225
|
+
);
|
|
1226
|
+
} finally {
|
|
1227
|
+
await session.close();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async listAnnotations(filter) {
|
|
1231
|
+
const session = this.getSession();
|
|
1232
|
+
try {
|
|
1233
|
+
const conditions = [];
|
|
1234
|
+
const params = {};
|
|
1235
|
+
if (filter.resourceId) {
|
|
1236
|
+
conditions.push("a.resourceId = $resourceId");
|
|
1237
|
+
params.resourceId = filter.resourceId;
|
|
1238
|
+
}
|
|
1239
|
+
if (filter.type) {
|
|
1240
|
+
const w3cType = filter.type === "highlight" ? "TextualBody" : "SpecificResource";
|
|
1241
|
+
conditions.push("a.type = $type");
|
|
1242
|
+
params.type = w3cType;
|
|
1243
|
+
}
|
|
1244
|
+
const whereClause = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
|
|
1245
|
+
const result = await session.run(
|
|
1246
|
+
`MATCH (a:Annotation) ${whereClause}
|
|
1247
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1248
|
+
RETURN a, collect(et.name) as entityTypes`,
|
|
1249
|
+
params
|
|
1250
|
+
);
|
|
1251
|
+
const annotations = result.records.map(
|
|
1252
|
+
(record) => this.parseAnnotationNode(record.get("a"), record.get("entityTypes"))
|
|
1253
|
+
);
|
|
1254
|
+
return { annotations, total: annotations.length };
|
|
1255
|
+
} finally {
|
|
1256
|
+
await session.close();
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
async getHighlights(resourceId) {
|
|
1260
|
+
const session = this.getSession();
|
|
1261
|
+
try {
|
|
1262
|
+
const result = await session.run(
|
|
1263
|
+
`MATCH (a:Annotation {resourceId: $resourceId})
|
|
1264
|
+
WHERE a.annotationCategory = 'highlight'
|
|
1265
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1266
|
+
RETURN a, collect(et.name) as entityTypes
|
|
1267
|
+
ORDER BY a.created DESC`,
|
|
1268
|
+
{ resourceId }
|
|
1269
|
+
);
|
|
1270
|
+
return result.records.map(
|
|
1271
|
+
(record) => this.parseAnnotationNode(record.get("a"), record.get("entityTypes"))
|
|
1272
|
+
);
|
|
1273
|
+
} finally {
|
|
1274
|
+
await session.close();
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
async resolveReference(annotationId, source) {
|
|
1278
|
+
const session = this.getSession();
|
|
1279
|
+
try {
|
|
1280
|
+
const docResult = await session.run(
|
|
1281
|
+
"MATCH (d:Resource {id: $id}) RETURN d.name as name",
|
|
1282
|
+
{ id: source }
|
|
1283
|
+
);
|
|
1284
|
+
const resourceName = docResult.records[0]?.get("name");
|
|
1285
|
+
const result = await session.run(
|
|
1286
|
+
`MATCH (a:Annotation {id: $annotationId})
|
|
1287
|
+
MATCH (to:Resource {id: $source})
|
|
1288
|
+
SET a.source = $source,
|
|
1289
|
+
a.resolvedResourceName = $resourceName,
|
|
1290
|
+
a.resolvedAt = datetime()
|
|
1291
|
+
MERGE (a)-[:REFERENCES]->(to)
|
|
1292
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1293
|
+
RETURN a, collect(et.name) as entityTypes`,
|
|
1294
|
+
{ annotationId, source, resourceName }
|
|
1295
|
+
);
|
|
1296
|
+
if (result.records.length === 0) {
|
|
1297
|
+
throw new Error("Annotation not found");
|
|
1298
|
+
}
|
|
1299
|
+
return this.parseAnnotationNode(
|
|
1300
|
+
result.records[0].get("a"),
|
|
1301
|
+
result.records[0].get("entityTypes")
|
|
1302
|
+
);
|
|
1303
|
+
} finally {
|
|
1304
|
+
await session.close();
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
async getReferences(resourceId) {
|
|
1308
|
+
const session = this.getSession();
|
|
1309
|
+
try {
|
|
1310
|
+
const result = await session.run(
|
|
1311
|
+
`MATCH (a:Annotation {resourceId: $resourceId})
|
|
1312
|
+
WHERE a.annotationCategory IN ['stub_reference', 'resolved_reference']
|
|
1313
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1314
|
+
RETURN a, collect(et.name) as entityTypes
|
|
1315
|
+
ORDER BY a.created DESC`,
|
|
1316
|
+
{ resourceId }
|
|
1317
|
+
);
|
|
1318
|
+
return result.records.map(
|
|
1319
|
+
(record) => this.parseAnnotationNode(record.get("a"), record.get("entityTypes"))
|
|
1320
|
+
);
|
|
1321
|
+
} finally {
|
|
1322
|
+
await session.close();
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
async getEntityReferences(resourceId, entityTypes) {
|
|
1326
|
+
const session = this.getSession();
|
|
1327
|
+
try {
|
|
1328
|
+
let cypher = `MATCH (a:Annotation {resourceId: $resourceId})
|
|
1329
|
+
WHERE a.source IS NOT NULL`;
|
|
1330
|
+
const params = { resourceId };
|
|
1331
|
+
if (entityTypes && entityTypes.length > 0) {
|
|
1332
|
+
cypher += `
|
|
1333
|
+
MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1334
|
+
WHERE et.name IN $entityTypes`;
|
|
1335
|
+
params.entityTypes = entityTypes;
|
|
1336
|
+
}
|
|
1337
|
+
cypher += `
|
|
1338
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et2:EntityType)
|
|
1339
|
+
RETURN a, collect(et2.name) as entityTypes
|
|
1340
|
+
ORDER BY a.created DESC`;
|
|
1341
|
+
const result = await session.run(cypher, params);
|
|
1342
|
+
return result.records.map(
|
|
1343
|
+
(record) => this.parseAnnotationNode(record.get("a"), record.get("entityTypes"))
|
|
1344
|
+
);
|
|
1345
|
+
} finally {
|
|
1346
|
+
await session.close();
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async getResourceAnnotations(resourceId) {
|
|
1350
|
+
const session = this.getSession();
|
|
1351
|
+
try {
|
|
1352
|
+
const result = await session.run(
|
|
1353
|
+
`MATCH (a:Annotation {resourceId: $resourceId})
|
|
1354
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1355
|
+
RETURN a, collect(et.name) as entityTypes
|
|
1356
|
+
ORDER BY a.created DESC`,
|
|
1357
|
+
{ resourceId }
|
|
1358
|
+
);
|
|
1359
|
+
return result.records.map(
|
|
1360
|
+
(record) => this.parseAnnotationNode(record.get("a"), record.get("entityTypes"))
|
|
1361
|
+
);
|
|
1362
|
+
} finally {
|
|
1363
|
+
await session.close();
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async getResourceReferencedBy(resourceUri3, motivation) {
|
|
1367
|
+
const session = this.getSession();
|
|
1368
|
+
try {
|
|
1369
|
+
const filterDesc = motivation ? ` with motivation=${motivation}` : "";
|
|
1370
|
+
console.log(`[Neo4j] getResourceReferencedBy: Searching for annotations${filterDesc} referencing ${resourceUri3}`);
|
|
1371
|
+
const motivationLabel = motivation ? `:${motivationToLabel(motivation)}` : "";
|
|
1372
|
+
const cypher = `MATCH (a:Annotation${motivationLabel})-[:REFERENCES]->(d:Resource {id: $resourceUri})
|
|
1373
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1374
|
+
RETURN a, collect(et.name) as entityTypes
|
|
1375
|
+
ORDER BY a.created DESC`;
|
|
1376
|
+
const result = await session.run(cypher, { resourceUri: resourceUri3 });
|
|
1377
|
+
console.log(`[Neo4j] getResourceReferencedBy: Found ${result.records.length} annotations`);
|
|
1378
|
+
return result.records.map(
|
|
1379
|
+
(record) => this.parseAnnotationNode(record.get("a"), record.get("entityTypes"))
|
|
1380
|
+
);
|
|
1381
|
+
} finally {
|
|
1382
|
+
await session.close();
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async getResourceConnections(resourceId) {
|
|
1386
|
+
const session = this.getSession();
|
|
1387
|
+
try {
|
|
1388
|
+
const result = await session.run(
|
|
1389
|
+
`MATCH (d:Resource {id: $resourceId})
|
|
1390
|
+
OPTIONAL MATCH (d)<-[:BELONGS_TO]-(a1:Annotation)-[:REFERENCES]->(other:Resource)
|
|
1391
|
+
OPTIONAL MATCH (other)<-[:BELONGS_TO]-(a2:Annotation)-[:REFERENCES]->(d)
|
|
1392
|
+
WITH other, COLLECT(DISTINCT a1) as outgoing, COLLECT(DISTINCT a2) as incoming
|
|
1393
|
+
WHERE other IS NOT NULL
|
|
1394
|
+
RETURN other, outgoing, incoming`,
|
|
1395
|
+
{ resourceId }
|
|
1396
|
+
);
|
|
1397
|
+
const connections = [];
|
|
1398
|
+
for (const record of result.records) {
|
|
1399
|
+
const targetResource = this.parseResourceNode(record.get("other"));
|
|
1400
|
+
const outgoingNodes = record.get("outgoing");
|
|
1401
|
+
const outgoing = [];
|
|
1402
|
+
for (const annNode of outgoingNodes) {
|
|
1403
|
+
const annId = annNode.properties.id;
|
|
1404
|
+
const annResult = await session.run(
|
|
1405
|
+
`MATCH (a:Annotation {id: $id})
|
|
1406
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1407
|
+
RETURN a, collect(et.name) as entityTypes`,
|
|
1408
|
+
{ id: annId }
|
|
1409
|
+
);
|
|
1410
|
+
if (annResult.records.length > 0) {
|
|
1411
|
+
outgoing.push(this.parseAnnotationNode(
|
|
1412
|
+
annResult.records[0].get("a"),
|
|
1413
|
+
annResult.records[0].get("entityTypes")
|
|
1414
|
+
));
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const incomingNodes = record.get("incoming");
|
|
1418
|
+
const incoming = [];
|
|
1419
|
+
for (const annNode of incomingNodes) {
|
|
1420
|
+
const annId = annNode.properties.id;
|
|
1421
|
+
const annResult = await session.run(
|
|
1422
|
+
`MATCH (a:Annotation {id: $id})
|
|
1423
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1424
|
+
RETURN a, collect(et.name) as entityTypes`,
|
|
1425
|
+
{ id: annId }
|
|
1426
|
+
);
|
|
1427
|
+
if (annResult.records.length > 0) {
|
|
1428
|
+
incoming.push(this.parseAnnotationNode(
|
|
1429
|
+
annResult.records[0].get("a"),
|
|
1430
|
+
annResult.records[0].get("entityTypes")
|
|
1431
|
+
));
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
connections.push({
|
|
1435
|
+
targetResource,
|
|
1436
|
+
annotations: outgoing,
|
|
1437
|
+
bidirectional: incoming.length > 0
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
return connections;
|
|
1441
|
+
} finally {
|
|
1442
|
+
await session.close();
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async findPath(fromResourceId, toResourceId, maxDepth = 5) {
|
|
1446
|
+
const session = this.getSession();
|
|
1447
|
+
try {
|
|
1448
|
+
const result = await session.run(
|
|
1449
|
+
`MATCH path = shortestPath((from:Resource {id: $fromId})-[:REFERENCES*..${maxDepth}]-(to:Resource {id: $toId}))
|
|
1450
|
+
WITH path, nodes(path) as docs, relationships(path) as rels
|
|
1451
|
+
RETURN docs, rels
|
|
1452
|
+
LIMIT 10`,
|
|
1453
|
+
{ fromId: fromResourceId, toId: toResourceId }
|
|
1454
|
+
);
|
|
1455
|
+
const paths = [];
|
|
1456
|
+
for (const record of result.records) {
|
|
1457
|
+
const docs = record.get("docs").map((node) => this.parseResourceNode(node));
|
|
1458
|
+
const rels = record.get("rels");
|
|
1459
|
+
const annotationIds = rels.map((rel) => rel.properties.id).filter((id) => id);
|
|
1460
|
+
const annotations = [];
|
|
1461
|
+
if (annotationIds.length > 0) {
|
|
1462
|
+
const selResult = await session.run(
|
|
1463
|
+
`MATCH (a:Annotation) WHERE a.id IN $ids
|
|
1464
|
+
OPTIONAL MATCH (a)-[:TAGGED_AS]->(et:EntityType)
|
|
1465
|
+
RETURN a, collect(et.name) as entityTypes`,
|
|
1466
|
+
{ ids: annotationIds }
|
|
1467
|
+
);
|
|
1468
|
+
selResult.records.forEach((rec) => {
|
|
1469
|
+
annotations.push(this.parseAnnotationNode(
|
|
1470
|
+
rec.get("a"),
|
|
1471
|
+
rec.get("entityTypes")
|
|
1472
|
+
));
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
paths.push({
|
|
1476
|
+
resources: docs,
|
|
1477
|
+
annotations
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
return paths;
|
|
1481
|
+
} finally {
|
|
1482
|
+
await session.close();
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async getEntityTypeStats() {
|
|
1486
|
+
const session = this.getSession();
|
|
1487
|
+
try {
|
|
1488
|
+
const result = await session.run(
|
|
1489
|
+
`MATCH (d:Resource)
|
|
1490
|
+
UNWIND d.entityTypes AS type
|
|
1491
|
+
RETURN type, count(*) AS count
|
|
1492
|
+
ORDER BY count DESC`
|
|
1493
|
+
);
|
|
1494
|
+
return result.records.map((record) => ({
|
|
1495
|
+
type: record.get("type"),
|
|
1496
|
+
count: record.get("count").toNumber()
|
|
1497
|
+
}));
|
|
1498
|
+
} finally {
|
|
1499
|
+
await session.close();
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async getStats() {
|
|
1503
|
+
const session = this.getSession();
|
|
1504
|
+
try {
|
|
1505
|
+
const docCountResult = await session.run("MATCH (d:Resource) RETURN count(d) as count");
|
|
1506
|
+
const resourceCount = docCountResult.records[0].get("count").toNumber();
|
|
1507
|
+
const selCountResult = await session.run("MATCH (a:Annotation) RETURN count(a) as count");
|
|
1508
|
+
const annotationCount = selCountResult.records[0].get("count").toNumber();
|
|
1509
|
+
const highlightCountResult = await session.run(
|
|
1510
|
+
"MATCH (a:Annotation) WHERE a.resolvedResourceId IS NULL RETURN count(a) as count"
|
|
1511
|
+
);
|
|
1512
|
+
const highlightCount = highlightCountResult.records[0].get("count").toNumber();
|
|
1513
|
+
const referenceCountResult = await session.run(
|
|
1514
|
+
"MATCH (a:Annotation) WHERE a.resolvedResourceId IS NOT NULL RETURN count(a) as count"
|
|
1515
|
+
);
|
|
1516
|
+
const referenceCount = referenceCountResult.records[0].get("count").toNumber();
|
|
1517
|
+
const entityRefCountResult = await session.run(
|
|
1518
|
+
"MATCH (a:Annotation) WHERE a.resolvedResourceId IS NOT NULL AND size(a.entityTypes) > 0 RETURN count(a) as count"
|
|
1519
|
+
);
|
|
1520
|
+
const entityReferenceCount = entityRefCountResult.records[0].get("count").toNumber();
|
|
1521
|
+
const entityTypeResult = await session.run(
|
|
1522
|
+
`MATCH (d:Resource)
|
|
1523
|
+
UNWIND d.entityTypes AS type
|
|
1524
|
+
RETURN type, count(*) AS count`
|
|
1525
|
+
);
|
|
1526
|
+
const entityTypes = {};
|
|
1527
|
+
entityTypeResult.records.forEach((record) => {
|
|
1528
|
+
entityTypes[record.get("type")] = record.get("count").toNumber();
|
|
1529
|
+
});
|
|
1530
|
+
const contentTypeResult = await session.run(
|
|
1531
|
+
`MATCH (d:Resource)
|
|
1532
|
+
RETURN d.format as type, count(*) AS count`
|
|
1533
|
+
);
|
|
1534
|
+
const contentTypes = {};
|
|
1535
|
+
contentTypeResult.records.forEach((record) => {
|
|
1536
|
+
contentTypes[record.get("type")] = record.get("count").toNumber();
|
|
1537
|
+
});
|
|
1538
|
+
return {
|
|
1539
|
+
resourceCount,
|
|
1540
|
+
annotationCount,
|
|
1541
|
+
highlightCount,
|
|
1542
|
+
referenceCount,
|
|
1543
|
+
entityReferenceCount,
|
|
1544
|
+
entityTypes,
|
|
1545
|
+
contentTypes
|
|
1546
|
+
};
|
|
1547
|
+
} finally {
|
|
1548
|
+
await session.close();
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
async createAnnotations(inputs) {
|
|
1552
|
+
const results = [];
|
|
1553
|
+
for (const input of inputs) {
|
|
1554
|
+
results.push(await this.createAnnotation(input));
|
|
1555
|
+
}
|
|
1556
|
+
return results;
|
|
1557
|
+
}
|
|
1558
|
+
async resolveReferences(inputs) {
|
|
1559
|
+
const results = [];
|
|
1560
|
+
for (const input of inputs) {
|
|
1561
|
+
results.push(await this.resolveReference(input.annotationId, input.source));
|
|
1562
|
+
}
|
|
1563
|
+
return results;
|
|
1564
|
+
}
|
|
1565
|
+
async detectAnnotations(_resourceId) {
|
|
1566
|
+
return [];
|
|
1567
|
+
}
|
|
1568
|
+
// Tag Collections
|
|
1569
|
+
async getEntityTypes() {
|
|
1570
|
+
if (this.entityTypesCollection === null) {
|
|
1571
|
+
await this.initializeTagCollections();
|
|
1572
|
+
}
|
|
1573
|
+
return Array.from(this.entityTypesCollection).sort();
|
|
1574
|
+
}
|
|
1575
|
+
async addEntityType(tag) {
|
|
1576
|
+
if (this.entityTypesCollection === null) {
|
|
1577
|
+
await this.initializeTagCollections();
|
|
1578
|
+
}
|
|
1579
|
+
this.entityTypesCollection.add(tag);
|
|
1580
|
+
await this.persistTagCollection("entity-types", this.entityTypesCollection);
|
|
1581
|
+
}
|
|
1582
|
+
async addEntityTypes(tags) {
|
|
1583
|
+
if (this.entityTypesCollection === null) {
|
|
1584
|
+
await this.initializeTagCollections();
|
|
1585
|
+
}
|
|
1586
|
+
tags.forEach((tag) => this.entityTypesCollection.add(tag));
|
|
1587
|
+
await this.persistTagCollection("entity-types", this.entityTypesCollection);
|
|
1588
|
+
}
|
|
1589
|
+
async initializeTagCollections() {
|
|
1590
|
+
const session = this.getSession();
|
|
1591
|
+
try {
|
|
1592
|
+
const result = await session.run(
|
|
1593
|
+
'MATCH (t:TagCollection {type: "entity-types"}) RETURN t.tags as tags'
|
|
1594
|
+
);
|
|
1595
|
+
let entityTypesFromDb = [];
|
|
1596
|
+
if (result.records.length > 0) {
|
|
1597
|
+
const record = result.records[0];
|
|
1598
|
+
if (record) {
|
|
1599
|
+
const tags = record.get("tags");
|
|
1600
|
+
entityTypesFromDb = tags || [];
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const { DEFAULT_ENTITY_TYPES } = await import("@semiont/ontology");
|
|
1604
|
+
this.entityTypesCollection = /* @__PURE__ */ new Set([...DEFAULT_ENTITY_TYPES, ...entityTypesFromDb]);
|
|
1605
|
+
await this.persistTagCollection("entity-types", this.entityTypesCollection);
|
|
1606
|
+
} finally {
|
|
1607
|
+
await session.close();
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
async persistTagCollection(type, collection) {
|
|
1611
|
+
const session = this.getSession();
|
|
1612
|
+
try {
|
|
1613
|
+
await session.run(
|
|
1614
|
+
"MERGE (t:TagCollection {type: $type}) SET t.tags = $tags",
|
|
1615
|
+
{ type, tags: Array.from(collection) }
|
|
1616
|
+
);
|
|
1617
|
+
} finally {
|
|
1618
|
+
await session.close();
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
generateId() {
|
|
1622
|
+
return uuidv42().replace(/-/g, "").substring(0, 12);
|
|
1623
|
+
}
|
|
1624
|
+
async clearDatabase() {
|
|
1625
|
+
const session = this.getSession();
|
|
1626
|
+
try {
|
|
1627
|
+
await session.run("MATCH (n) DETACH DELETE n");
|
|
1628
|
+
this.entityTypesCollection = null;
|
|
1629
|
+
} finally {
|
|
1630
|
+
await session.close();
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
// Helper methods to parse Neo4j nodes
|
|
1634
|
+
parseResourceNode(node) {
|
|
1635
|
+
const props = node.properties;
|
|
1636
|
+
if (!props.id) throw new Error("Resource missing required field: id");
|
|
1637
|
+
if (!props.name) throw new Error(`Resource ${props.id} missing required field: name`);
|
|
1638
|
+
if (!props.entityTypes) throw new Error(`Resource ${props.id} missing required field: entityTypes`);
|
|
1639
|
+
if (!props.format) throw new Error(`Resource ${props.id} missing required field: contentType`);
|
|
1640
|
+
if (props.archived === void 0 || props.archived === null) throw new Error(`Resource ${props.id} missing required field: archived`);
|
|
1641
|
+
if (!props.created) throw new Error(`Resource ${props.id} missing required field: created`);
|
|
1642
|
+
if (!props.creator) throw new Error(`Resource ${props.id} missing required field: creator`);
|
|
1643
|
+
if (!props.creationMethod) throw new Error(`Resource ${props.id} missing required field: creationMethod`);
|
|
1644
|
+
if (!props.contentChecksum) throw new Error(`Resource ${props.id} missing required field: contentChecksum`);
|
|
1645
|
+
const resource = {
|
|
1646
|
+
"@context": "https://schema.org/",
|
|
1647
|
+
"@id": props.id,
|
|
1648
|
+
name: props.name,
|
|
1649
|
+
entityTypes: props.entityTypes,
|
|
1650
|
+
representations: [{
|
|
1651
|
+
mediaType: props.format,
|
|
1652
|
+
checksum: props.contentChecksum,
|
|
1653
|
+
rel: "original"
|
|
1654
|
+
}],
|
|
1655
|
+
archived: props.archived,
|
|
1656
|
+
dateCreated: props.created.toString(),
|
|
1657
|
+
wasAttributedTo: typeof props.creator === "string" ? JSON.parse(props.creator) : props.creator,
|
|
1658
|
+
creationMethod: props.creationMethod
|
|
1659
|
+
};
|
|
1660
|
+
if (props.sourceResourceId) resource.sourceResourceId = props.sourceResourceId;
|
|
1661
|
+
return resource;
|
|
1662
|
+
}
|
|
1663
|
+
parseAnnotationNode(node, entityTypes = []) {
|
|
1664
|
+
const props = node.properties;
|
|
1665
|
+
if (!props.id) throw new Error("Annotation missing required field: id");
|
|
1666
|
+
if (!props.resourceId) throw new Error(`Annotation ${props.id} missing required field: resourceId`);
|
|
1667
|
+
if (!props.type) throw new Error(`Annotation ${props.id} missing required field: type`);
|
|
1668
|
+
if (!props.selector) throw new Error(`Annotation ${props.id} missing required field: selector`);
|
|
1669
|
+
if (!props.creator) throw new Error(`Annotation ${props.id} missing required field: creator`);
|
|
1670
|
+
if (!props.motivation) throw new Error(`Annotation ${props.id} missing required field: motivation`);
|
|
1671
|
+
const creator = JSON.parse(props.creator);
|
|
1672
|
+
const bodyArray = [];
|
|
1673
|
+
for (const entityType of entityTypes) {
|
|
1674
|
+
if (entityType) {
|
|
1675
|
+
bodyArray.push({
|
|
1676
|
+
type: "TextualBody",
|
|
1677
|
+
value: entityType,
|
|
1678
|
+
purpose: "tagging"
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
if (props.source) {
|
|
1683
|
+
bodyArray.push({
|
|
1684
|
+
type: "SpecificResource",
|
|
1685
|
+
source: props.source,
|
|
1686
|
+
purpose: "linking"
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
const annotation = {
|
|
1690
|
+
"@context": "http://www.w3.org/ns/anno.jsonld",
|
|
1691
|
+
"type": "Annotation",
|
|
1692
|
+
id: props.id,
|
|
1693
|
+
motivation: props.motivation,
|
|
1694
|
+
target: {
|
|
1695
|
+
source: props.resourceId,
|
|
1696
|
+
selector: JSON.parse(props.selector)
|
|
1697
|
+
},
|
|
1698
|
+
body: bodyArray,
|
|
1699
|
+
creator,
|
|
1700
|
+
created: props.created
|
|
1701
|
+
// ISO string from DB
|
|
1702
|
+
};
|
|
1703
|
+
if (props.modified) annotation.modified = props.modified.toString();
|
|
1704
|
+
if (props.generator) {
|
|
1705
|
+
try {
|
|
1706
|
+
annotation.generator = JSON.parse(props.generator);
|
|
1707
|
+
} catch (e) {
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return annotation;
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1714
|
+
// src/implementations/janusgraph.ts
|
|
1715
|
+
import gremlin2 from "gremlin";
|
|
1716
|
+
import {
|
|
1717
|
+
getBodySource as getBodySource3,
|
|
1718
|
+
getPrimaryRepresentation as getPrimaryRepresentation3,
|
|
1719
|
+
getResourceId as getResourceId2,
|
|
1720
|
+
getExactText as getExactText3,
|
|
1721
|
+
resourceUri
|
|
1722
|
+
} from "@semiont/api-client";
|
|
1723
|
+
import { getEntityTypes as getEntityTypes3 } from "@semiont/ontology";
|
|
1724
|
+
import { annotationIdToURI } from "@semiont/core";
|
|
1725
|
+
import { v4 as uuidv43 } from "uuid";
|
|
1726
|
+
var traversal = gremlin2.process.AnonymousTraversalSource.traversal;
|
|
1727
|
+
var DriverRemoteConnection = gremlin2.driver.DriverRemoteConnection;
|
|
1728
|
+
var JanusGraphDatabase = class {
|
|
1729
|
+
constructor(graphConfig, envConfig) {
|
|
1730
|
+
this.graphConfig = graphConfig;
|
|
1731
|
+
this.envConfig = envConfig;
|
|
1732
|
+
}
|
|
1733
|
+
connected = false;
|
|
1734
|
+
connection = null;
|
|
1735
|
+
g = null;
|
|
1736
|
+
// Tag Collections - cached in memory for performance
|
|
1737
|
+
entityTypesCollection = null;
|
|
1738
|
+
async connect() {
|
|
1739
|
+
const host = this.graphConfig.host;
|
|
1740
|
+
if (!host) {
|
|
1741
|
+
throw new Error("JanusGraph host is required: provide in config");
|
|
1742
|
+
}
|
|
1743
|
+
const port = this.graphConfig.port;
|
|
1744
|
+
if (!port) {
|
|
1745
|
+
throw new Error("JanusGraph port is required: provide in config");
|
|
1746
|
+
}
|
|
1747
|
+
console.log(`Attempting to connect to JanusGraph at ws://${host}:${port}/gremlin`);
|
|
1748
|
+
this.connection = new DriverRemoteConnection(
|
|
1749
|
+
`ws://${host}:${port}/gremlin`,
|
|
1750
|
+
{}
|
|
1751
|
+
);
|
|
1752
|
+
this.g = traversal().withRemote(this.connection);
|
|
1753
|
+
await this.g.V().limit(1).toList();
|
|
1754
|
+
this.connected = true;
|
|
1755
|
+
console.log("Successfully connected to JanusGraph");
|
|
1756
|
+
await this.initializeSchema();
|
|
1757
|
+
}
|
|
1758
|
+
async disconnect() {
|
|
1759
|
+
if (this.connection) {
|
|
1760
|
+
await this.connection.close();
|
|
1761
|
+
}
|
|
1762
|
+
this.connected = false;
|
|
1763
|
+
}
|
|
1764
|
+
isConnected() {
|
|
1765
|
+
return this.connected;
|
|
1766
|
+
}
|
|
1767
|
+
async initializeSchema() {
|
|
1768
|
+
console.log("Schema initialization would happen here in production");
|
|
1769
|
+
}
|
|
1770
|
+
// Helper function to convert vertex to Resource
|
|
1771
|
+
vertexToResource(vertex) {
|
|
1772
|
+
const props = vertex.properties || {};
|
|
1773
|
+
const id = this.getPropertyValue(props, "id");
|
|
1774
|
+
const creatorRaw = this.getPropertyValue(props, "creator");
|
|
1775
|
+
const creationMethod = this.getPropertyValue(props, "creationMethod");
|
|
1776
|
+
const contentChecksum = this.getPropertyValue(props, "contentChecksum");
|
|
1777
|
+
const mediaType = this.getPropertyValue(props, "contentType");
|
|
1778
|
+
if (!creatorRaw) throw new Error(`Resource ${id} missing required field: creator`);
|
|
1779
|
+
if (!creationMethod) throw new Error(`Resource ${id} missing required field: creationMethod`);
|
|
1780
|
+
if (!contentChecksum) throw new Error(`Resource ${id} missing required field: contentChecksum`);
|
|
1781
|
+
if (!mediaType) throw new Error(`Resource ${id} missing required field: contentType`);
|
|
1782
|
+
const creator = typeof creatorRaw === "string" ? JSON.parse(creatorRaw) : creatorRaw;
|
|
1783
|
+
const resource = {
|
|
1784
|
+
"@context": "https://schema.org/",
|
|
1785
|
+
"@id": id,
|
|
1786
|
+
name: this.getPropertyValue(props, "name"),
|
|
1787
|
+
entityTypes: JSON.parse(this.getPropertyValue(props, "entityTypes") || "[]"),
|
|
1788
|
+
representations: [{
|
|
1789
|
+
mediaType,
|
|
1790
|
+
checksum: contentChecksum,
|
|
1791
|
+
rel: "original"
|
|
1792
|
+
}],
|
|
1793
|
+
archived: this.getPropertyValue(props, "archived") === "true",
|
|
1794
|
+
dateCreated: this.getPropertyValue(props, "created"),
|
|
1795
|
+
wasAttributedTo: creator,
|
|
1796
|
+
creationMethod
|
|
1797
|
+
};
|
|
1798
|
+
const sourceAnnotationId = this.getPropertyValue(props, "sourceAnnotationId");
|
|
1799
|
+
const sourceResourceId = this.getPropertyValue(props, "sourceResourceId");
|
|
1800
|
+
if (sourceAnnotationId) resource.sourceAnnotationId = sourceAnnotationId;
|
|
1801
|
+
if (sourceResourceId) resource.sourceResourceId = sourceResourceId;
|
|
1802
|
+
return resource;
|
|
1803
|
+
}
|
|
1804
|
+
// Helper to get property value from Gremlin vertex properties
|
|
1805
|
+
getPropertyValue(props, key) {
|
|
1806
|
+
if (!props[key]) return void 0;
|
|
1807
|
+
const prop = Array.isArray(props[key]) ? props[key][0] : props[key];
|
|
1808
|
+
return prop?.value || prop;
|
|
1809
|
+
}
|
|
1810
|
+
// Helper method to fetch annotations with their entity types
|
|
1811
|
+
async fetchAnnotationsWithEntityTypes(annotationVertices) {
|
|
1812
|
+
const annotations = [];
|
|
1813
|
+
for (const vertex of annotationVertices) {
|
|
1814
|
+
const id = this.getPropertyValue(vertex.properties || {}, "id");
|
|
1815
|
+
const entityTypeVertices = await this.g.V().has("Annotation", "id", id).out("TAGGED_AS").has("EntityType").toList();
|
|
1816
|
+
const entityTypes = entityTypeVertices.map(
|
|
1817
|
+
(v) => this.getPropertyValue(v.properties || {}, "name")
|
|
1818
|
+
).filter(Boolean);
|
|
1819
|
+
annotations.push(this.vertexToAnnotation(vertex, entityTypes));
|
|
1820
|
+
}
|
|
1821
|
+
return annotations;
|
|
1822
|
+
}
|
|
1823
|
+
// Helper function to convert vertex to Annotation
|
|
1824
|
+
vertexToAnnotation(vertex, entityTypes = []) {
|
|
1825
|
+
const props = vertex.properties || {};
|
|
1826
|
+
const motivation = this.getPropertyValue(props, "motivation") || "linking";
|
|
1827
|
+
const creatorJson = this.getPropertyValue(props, "creator");
|
|
1828
|
+
const creator = JSON.parse(creatorJson);
|
|
1829
|
+
const bodyArray = [];
|
|
1830
|
+
for (const entityType of entityTypes) {
|
|
1831
|
+
if (entityType) {
|
|
1832
|
+
bodyArray.push({
|
|
1833
|
+
type: "TextualBody",
|
|
1834
|
+
value: entityType,
|
|
1835
|
+
purpose: "tagging"
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
const bodySource = this.getPropertyValue(props, "source");
|
|
1840
|
+
if (bodySource) {
|
|
1841
|
+
bodyArray.push({
|
|
1842
|
+
type: "SpecificResource",
|
|
1843
|
+
source: bodySource,
|
|
1844
|
+
purpose: "linking"
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
const annotation = {
|
|
1848
|
+
"@context": "http://www.w3.org/ns/anno.jsonld",
|
|
1849
|
+
"type": "Annotation",
|
|
1850
|
+
id: this.getPropertyValue(props, "id"),
|
|
1851
|
+
motivation,
|
|
1852
|
+
target: {
|
|
1853
|
+
source: this.getPropertyValue(props, "resourceId"),
|
|
1854
|
+
selector: JSON.parse(this.getPropertyValue(props, "selector") || "{}")
|
|
1855
|
+
},
|
|
1856
|
+
body: bodyArray,
|
|
1857
|
+
creator,
|
|
1858
|
+
created: this.getPropertyValue(props, "created")
|
|
1859
|
+
// ISO string from DB
|
|
1860
|
+
};
|
|
1861
|
+
const modified = this.getPropertyValue(props, "modified");
|
|
1862
|
+
if (modified) {
|
|
1863
|
+
annotation.modified = modified;
|
|
1864
|
+
}
|
|
1865
|
+
const generatorJson = this.getPropertyValue(props, "generator");
|
|
1866
|
+
if (generatorJson) {
|
|
1867
|
+
try {
|
|
1868
|
+
annotation.generator = JSON.parse(generatorJson);
|
|
1869
|
+
} catch (e) {
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
return annotation;
|
|
1873
|
+
}
|
|
1874
|
+
async createResource(resource) {
|
|
1875
|
+
const id = getResourceId2(resource);
|
|
1876
|
+
const primaryRep = getPrimaryRepresentation3(resource);
|
|
1877
|
+
if (!primaryRep) {
|
|
1878
|
+
throw new Error("Resource must have at least one representation");
|
|
1879
|
+
}
|
|
1880
|
+
const vertex = this.g.addV("Resource").property("id", id).property("name", resource.name).property("entityTypes", JSON.stringify(resource.entityTypes)).property("contentType", primaryRep.mediaType).property("archived", resource.archived || false).property("created", resource.dateCreated).property("creator", JSON.stringify(resource.wasAttributedTo)).property("creationMethod", resource.creationMethod).property("contentChecksum", primaryRep.checksum);
|
|
1881
|
+
if (resource.sourceAnnotationId) {
|
|
1882
|
+
vertex.property("sourceAnnotationId", resource.sourceAnnotationId);
|
|
1883
|
+
}
|
|
1884
|
+
if (resource.sourceResourceId) {
|
|
1885
|
+
vertex.property("sourceResourceId", resource.sourceResourceId);
|
|
1886
|
+
}
|
|
1887
|
+
await vertex.next();
|
|
1888
|
+
console.log("Created resource vertex in JanusGraph:", id);
|
|
1889
|
+
return resource;
|
|
1890
|
+
}
|
|
1891
|
+
async getResource(id) {
|
|
1892
|
+
const vertices = await this.g.V().has("Resource", "id", id).toList();
|
|
1893
|
+
if (vertices.length === 0) {
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
return this.vertexToResource(vertices[0]);
|
|
1897
|
+
}
|
|
1898
|
+
async updateResource(id, input) {
|
|
1899
|
+
if (Object.keys(input).length !== 1 || input.archived === void 0) {
|
|
1900
|
+
throw new Error("Resources are immutable. Only archiving is allowed.");
|
|
1901
|
+
}
|
|
1902
|
+
await this.g.V().has("Resource", "id", id).property("archived", input.archived).next();
|
|
1903
|
+
const updatedResource = await this.getResource(id);
|
|
1904
|
+
if (!updatedResource) {
|
|
1905
|
+
throw new Error("Resource not found");
|
|
1906
|
+
}
|
|
1907
|
+
return updatedResource;
|
|
1908
|
+
}
|
|
1909
|
+
async deleteResource(id) {
|
|
1910
|
+
await this.g.V().has("Resource", "id", id).drop().next();
|
|
1911
|
+
console.log("Deleted resource from JanusGraph:", id);
|
|
1912
|
+
}
|
|
1913
|
+
async listResources(filter) {
|
|
1914
|
+
let traversalQuery = this.g.V().hasLabel("Resource");
|
|
1915
|
+
if (filter.search) {
|
|
1916
|
+
traversalQuery = traversalQuery.has("name", gremlin2.process.TextP.containing(filter.search));
|
|
1917
|
+
}
|
|
1918
|
+
const docs = await traversalQuery.toList();
|
|
1919
|
+
let resources = docs.map((v) => this.vertexToResource(v));
|
|
1920
|
+
if (filter.entityTypes && filter.entityTypes.length > 0) {
|
|
1921
|
+
resources = resources.filter(
|
|
1922
|
+
(doc) => filter.entityTypes.some((type) => doc.entityTypes?.includes(type))
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
const total = resources.length;
|
|
1926
|
+
const offset = filter.offset || 0;
|
|
1927
|
+
const limit = filter.limit || 50;
|
|
1928
|
+
return {
|
|
1929
|
+
resources: resources.slice(offset, offset + limit),
|
|
1930
|
+
total
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
async searchResources(query, limit) {
|
|
1934
|
+
const result = await this.listResources({ search: query, limit: limit || 10 });
|
|
1935
|
+
return result.resources;
|
|
1936
|
+
}
|
|
1937
|
+
async createAnnotation(input) {
|
|
1938
|
+
const id = this.generateId();
|
|
1939
|
+
const motivation = input.motivation;
|
|
1940
|
+
const annotation = {
|
|
1941
|
+
"@context": "http://www.w3.org/ns/anno.jsonld",
|
|
1942
|
+
"type": "Annotation",
|
|
1943
|
+
id,
|
|
1944
|
+
motivation,
|
|
1945
|
+
target: input.target,
|
|
1946
|
+
body: input.body,
|
|
1947
|
+
creator: input.creator,
|
|
1948
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
1949
|
+
};
|
|
1950
|
+
const bodySource = getBodySource3(input.body);
|
|
1951
|
+
const entityTypes = getEntityTypes3(input);
|
|
1952
|
+
const bodyType = Array.isArray(input.body) ? "SpecificResource" : input.body.type;
|
|
1953
|
+
const targetSource = typeof input.target === "string" ? input.target : input.target.source;
|
|
1954
|
+
const targetSelector = typeof input.target === "string" ? void 0 : input.target.selector;
|
|
1955
|
+
const vertex = this.g.addV("Annotation").property("id", id).property("resourceId", targetSource).property("text", targetSelector ? getExactText3(targetSelector) : "").property("selector", JSON.stringify(targetSelector || {})).property("type", bodyType).property("motivation", motivation).property("creator", JSON.stringify(input.creator)).property("created", annotation.created);
|
|
1956
|
+
if (bodySource) {
|
|
1957
|
+
vertex.property("source", bodySource);
|
|
1958
|
+
}
|
|
1959
|
+
const annVertex = await vertex.next();
|
|
1960
|
+
await this.g.V(annVertex.value).addE("BELONGS_TO").to(this.g.V().has("Resource", "id", targetSource)).next();
|
|
1961
|
+
if (bodySource) {
|
|
1962
|
+
await this.g.V(annVertex.value).addE("REFERENCES").to(this.g.V().has("Resource", "id", bodySource)).next();
|
|
1963
|
+
}
|
|
1964
|
+
for (const entityType of entityTypes) {
|
|
1965
|
+
const etResults = await this.g.V().has("EntityType", "name", entityType).toList();
|
|
1966
|
+
let etVertex;
|
|
1967
|
+
if (etResults.length === 0) {
|
|
1968
|
+
etVertex = await this.g.addV("EntityType").property("name", entityType).next();
|
|
1969
|
+
} else {
|
|
1970
|
+
etVertex = { value: etResults[0] };
|
|
1971
|
+
}
|
|
1972
|
+
await this.g.V(annVertex.value).addE("TAGGED_AS").to(this.g.V(etVertex.value)).next();
|
|
1973
|
+
}
|
|
1974
|
+
console.log("Created annotation in JanusGraph:", id);
|
|
1975
|
+
return annotation;
|
|
1976
|
+
}
|
|
1977
|
+
async getAnnotation(id) {
|
|
1978
|
+
const vertices = await this.g.V().has("Annotation", "id", id).toList();
|
|
1979
|
+
if (vertices.length === 0) {
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
const entityTypeVertices = await this.g.V().has("Annotation", "id", id).out("TAGGED_AS").has("EntityType").toList();
|
|
1983
|
+
const entityTypes = entityTypeVertices.map(
|
|
1984
|
+
(v) => this.getPropertyValue(v.properties || {}, "name")
|
|
1985
|
+
).filter(Boolean);
|
|
1986
|
+
return this.vertexToAnnotation(vertices[0], entityTypes);
|
|
1987
|
+
}
|
|
1988
|
+
async updateAnnotation(id, updates) {
|
|
1989
|
+
const traversalQuery = this.g.V().has("Annotation", "id", id);
|
|
1990
|
+
if (updates.target !== void 0 && typeof updates.target !== "string") {
|
|
1991
|
+
if (updates.target.selector !== void 0) {
|
|
1992
|
+
await traversalQuery.property("text", getExactText3(updates.target.selector)).next();
|
|
1993
|
+
await traversalQuery.property("selector", JSON.stringify(updates.target.selector)).next();
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
if (updates.body !== void 0) {
|
|
1997
|
+
const bodySource = getBodySource3(updates.body);
|
|
1998
|
+
const entityTypes = getEntityTypes3({ body: updates.body });
|
|
1999
|
+
if (bodySource) {
|
|
2000
|
+
await traversalQuery.property("source", bodySource).next();
|
|
2001
|
+
}
|
|
2002
|
+
if (entityTypes.length >= 0) {
|
|
2003
|
+
await this.g.V().has("Annotation", "id", id).outE("TAGGED_AS").drop().iterate();
|
|
2004
|
+
for (const entityType of entityTypes) {
|
|
2005
|
+
const etResults = await this.g.V().has("EntityType", "name", entityType).toList();
|
|
2006
|
+
let etVertex;
|
|
2007
|
+
if (etResults.length === 0) {
|
|
2008
|
+
etVertex = await this.g.addV("EntityType").property("name", entityType).next();
|
|
2009
|
+
} else {
|
|
2010
|
+
etVertex = { value: etResults[0] };
|
|
2011
|
+
}
|
|
2012
|
+
const annVertices = await this.g.V().has("Annotation", "id", id).toList();
|
|
2013
|
+
if (annVertices.length > 0) {
|
|
2014
|
+
await this.g.V(annVertices[0]).addE("TAGGED_AS").to(this.g.V(etVertex.value)).next();
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (updates.modified !== void 0) {
|
|
2020
|
+
await traversalQuery.property("modified", updates.modified).next();
|
|
2021
|
+
}
|
|
2022
|
+
if (updates.generator !== void 0) {
|
|
2023
|
+
await traversalQuery.property("generator", JSON.stringify(updates.generator)).next();
|
|
2024
|
+
}
|
|
2025
|
+
const updatedAnnotation = await this.getAnnotation(id);
|
|
2026
|
+
if (!updatedAnnotation) {
|
|
2027
|
+
throw new Error("Annotation not found");
|
|
2028
|
+
}
|
|
2029
|
+
return updatedAnnotation;
|
|
2030
|
+
}
|
|
2031
|
+
async deleteAnnotation(id) {
|
|
2032
|
+
await this.g.V().has("Annotation", "id", id).drop().next();
|
|
2033
|
+
console.log("Deleted annotation from JanusGraph:", id);
|
|
2034
|
+
}
|
|
2035
|
+
async listAnnotations(filter) {
|
|
2036
|
+
let traversalQuery = this.g.V().hasLabel("Annotation");
|
|
2037
|
+
if (filter.resourceId) {
|
|
2038
|
+
traversalQuery = traversalQuery.has("resourceId", filter.resourceId);
|
|
2039
|
+
}
|
|
2040
|
+
if (filter.type) {
|
|
2041
|
+
const w3cType = filter.type === "highlight" ? "TextualBody" : "SpecificResource";
|
|
2042
|
+
traversalQuery = traversalQuery.has("type", w3cType);
|
|
2043
|
+
}
|
|
2044
|
+
const vertices = await traversalQuery.toList();
|
|
2045
|
+
const annotations = await this.fetchAnnotationsWithEntityTypes(vertices);
|
|
2046
|
+
return {
|
|
2047
|
+
annotations,
|
|
2048
|
+
total: annotations.length
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
async getHighlights(resourceId) {
|
|
2052
|
+
const { annotations } = await this.listAnnotations({
|
|
2053
|
+
resourceId,
|
|
2054
|
+
type: "highlight"
|
|
2055
|
+
});
|
|
2056
|
+
return annotations;
|
|
2057
|
+
}
|
|
2058
|
+
async resolveReference(annotationId, source) {
|
|
2059
|
+
const publicURL = this.envConfig.services.backend.publicURL;
|
|
2060
|
+
const annotation = await this.getAnnotation(annotationIdToURI(annotationId, publicURL));
|
|
2061
|
+
if (!annotation) throw new Error("Annotation not found");
|
|
2062
|
+
await this.updateAnnotation(annotationIdToURI(annotationId, publicURL), {
|
|
2063
|
+
body: [
|
|
2064
|
+
{
|
|
2065
|
+
type: "SpecificResource",
|
|
2066
|
+
source,
|
|
2067
|
+
purpose: "linking"
|
|
2068
|
+
}
|
|
2069
|
+
]
|
|
2070
|
+
});
|
|
2071
|
+
await this.g.V().has("Annotation", "id", annotationId).addE("REFERENCES").to(this.g.V().has("Resource", "id", source)).next();
|
|
2072
|
+
const updatedAnnotation = await this.getAnnotation(annotationIdToURI(annotationId, publicURL));
|
|
2073
|
+
if (!updatedAnnotation) {
|
|
2074
|
+
throw new Error("Annotation not found after update");
|
|
2075
|
+
}
|
|
2076
|
+
return updatedAnnotation;
|
|
2077
|
+
}
|
|
2078
|
+
async getReferences(resourceId) {
|
|
2079
|
+
const { annotations } = await this.listAnnotations({
|
|
2080
|
+
resourceId,
|
|
2081
|
+
type: "reference"
|
|
2082
|
+
});
|
|
2083
|
+
return annotations;
|
|
2084
|
+
}
|
|
2085
|
+
async getEntityReferences(resourceId, entityTypes) {
|
|
2086
|
+
const { annotations } = await this.listAnnotations({
|
|
2087
|
+
resourceId,
|
|
2088
|
+
type: "reference"
|
|
2089
|
+
});
|
|
2090
|
+
if (entityTypes && entityTypes.length > 0) {
|
|
2091
|
+
return annotations.filter((ann) => {
|
|
2092
|
+
const annEntityTypes = getEntityTypes3(ann);
|
|
2093
|
+
return annEntityTypes.some((type) => entityTypes.includes(type));
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
return annotations.filter((ann) => getEntityTypes3(ann).length > 0);
|
|
2097
|
+
}
|
|
2098
|
+
async getResourceAnnotations(resourceId) {
|
|
2099
|
+
const { annotations } = await this.listAnnotations({ resourceId });
|
|
2100
|
+
return annotations;
|
|
2101
|
+
}
|
|
2102
|
+
async getResourceReferencedBy(resourceUri3, _motivation) {
|
|
2103
|
+
const vertices = await this.g.V().hasLabel("Annotation").has("source", resourceUri3).toList();
|
|
2104
|
+
return await this.fetchAnnotationsWithEntityTypes(vertices);
|
|
2105
|
+
}
|
|
2106
|
+
async getResourceConnections(resourceId) {
|
|
2107
|
+
const paths = await this.g.V().has("Resource", "id", resourceId).inE("BELONGS_TO").outV().outE("REFERENCES").inV().path().toList();
|
|
2108
|
+
console.log("Found paths:", paths.length);
|
|
2109
|
+
const connections = [];
|
|
2110
|
+
const refs = await this.getReferences(resourceId);
|
|
2111
|
+
for (const ref of refs) {
|
|
2112
|
+
const bodySource = getBodySource3(ref.body);
|
|
2113
|
+
if (bodySource) {
|
|
2114
|
+
const targetDoc = await this.getResource(resourceUri(bodySource));
|
|
2115
|
+
if (targetDoc) {
|
|
2116
|
+
const existing = connections.find((c) => c.targetResource.id === targetDoc.id);
|
|
2117
|
+
if (existing) {
|
|
2118
|
+
existing.annotations.push(ref);
|
|
2119
|
+
} else {
|
|
2120
|
+
connections.push({
|
|
2121
|
+
targetResource: targetDoc,
|
|
2122
|
+
annotations: [ref],
|
|
2123
|
+
relationshipType: void 0,
|
|
2124
|
+
bidirectional: false
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return connections;
|
|
2131
|
+
}
|
|
2132
|
+
async findPath(_fromResourceId, _toResourceId, _maxDepth) {
|
|
2133
|
+
return [];
|
|
2134
|
+
}
|
|
2135
|
+
async getEntityTypeStats() {
|
|
2136
|
+
const docs = await this.g.V().hasLabel("Resource").toList();
|
|
2137
|
+
const resources = docs.map((v) => this.vertexToResource(v));
|
|
2138
|
+
const stats = /* @__PURE__ */ new Map();
|
|
2139
|
+
for (const doc of resources) {
|
|
2140
|
+
for (const type of doc.entityTypes || []) {
|
|
2141
|
+
stats.set(type, (stats.get(type) || 0) + 1);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
return Array.from(stats.entries()).map(([type, count]) => ({ type, count }));
|
|
2145
|
+
}
|
|
2146
|
+
async getStats() {
|
|
2147
|
+
const entityTypes = {};
|
|
2148
|
+
const contentTypes = {};
|
|
2149
|
+
const docs = await this.g.V().hasLabel("Resource").toList();
|
|
2150
|
+
const resources = docs.map((v) => this.vertexToResource(v));
|
|
2151
|
+
for (const doc of resources) {
|
|
2152
|
+
for (const type of doc.entityTypes || []) {
|
|
2153
|
+
entityTypes[type] = (entityTypes[type] || 0) + 1;
|
|
2154
|
+
}
|
|
2155
|
+
const primaryRep = getPrimaryRepresentation3(doc);
|
|
2156
|
+
if (primaryRep?.mediaType) {
|
|
2157
|
+
contentTypes[primaryRep.mediaType] = (contentTypes[primaryRep.mediaType] || 0) + 1;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const anns = await this.g.V().hasLabel("Annotation").toList();
|
|
2161
|
+
const annotations = await this.fetchAnnotationsWithEntityTypes(anns);
|
|
2162
|
+
const highlights = annotations.filter((a) => a.motivation === "highlighting");
|
|
2163
|
+
const references = annotations.filter((a) => a.motivation === "linking");
|
|
2164
|
+
const entityReferences = references.filter((a) => getEntityTypes3(a).length > 0);
|
|
2165
|
+
return {
|
|
2166
|
+
resourceCount: resources.length,
|
|
2167
|
+
annotationCount: annotations.length,
|
|
2168
|
+
highlightCount: highlights.length,
|
|
2169
|
+
referenceCount: references.length,
|
|
2170
|
+
entityReferenceCount: entityReferences.length,
|
|
2171
|
+
entityTypes,
|
|
2172
|
+
contentTypes
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
async createAnnotations(inputs) {
|
|
2176
|
+
const results = [];
|
|
2177
|
+
for (const input of inputs) {
|
|
2178
|
+
results.push(await this.createAnnotation(input));
|
|
2179
|
+
}
|
|
2180
|
+
return results;
|
|
2181
|
+
}
|
|
2182
|
+
async resolveReferences(inputs) {
|
|
2183
|
+
const results = [];
|
|
2184
|
+
for (const input of inputs) {
|
|
2185
|
+
results.push(await this.resolveReference(input.annotationId, input.source));
|
|
2186
|
+
}
|
|
2187
|
+
return results;
|
|
2188
|
+
}
|
|
2189
|
+
async detectAnnotations(_resourceId) {
|
|
2190
|
+
return [];
|
|
2191
|
+
}
|
|
2192
|
+
async getEntityTypes() {
|
|
2193
|
+
if (this.entityTypesCollection === null) {
|
|
2194
|
+
await this.initializeTagCollections();
|
|
2195
|
+
}
|
|
2196
|
+
return Array.from(this.entityTypesCollection).sort();
|
|
2197
|
+
}
|
|
2198
|
+
async addEntityType(tag) {
|
|
2199
|
+
if (this.entityTypesCollection === null) {
|
|
2200
|
+
await this.initializeTagCollections();
|
|
2201
|
+
}
|
|
2202
|
+
this.entityTypesCollection.add(tag);
|
|
2203
|
+
try {
|
|
2204
|
+
const existing = await this.g.V().hasLabel("TagCollection").has("type", "entity-types").toList();
|
|
2205
|
+
if (existing.length > 0) {
|
|
2206
|
+
await this.g.V(existing[0]).property("tags", JSON.stringify(Array.from(this.entityTypesCollection))).next();
|
|
2207
|
+
} else {
|
|
2208
|
+
await this.g.addV("TagCollection").property("type", "entity-types").property("tags", JSON.stringify(Array.from(this.entityTypesCollection))).next();
|
|
2209
|
+
}
|
|
2210
|
+
} catch (error) {
|
|
2211
|
+
console.error("Failed to add entity type:", error);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
async addEntityTypes(tags) {
|
|
2215
|
+
if (this.entityTypesCollection === null) {
|
|
2216
|
+
await this.initializeTagCollections();
|
|
2217
|
+
}
|
|
2218
|
+
tags.forEach((tag) => this.entityTypesCollection.add(tag));
|
|
2219
|
+
try {
|
|
2220
|
+
const existing = await this.g.V().hasLabel("TagCollection").has("type", "entity-types").toList();
|
|
2221
|
+
if (existing.length > 0) {
|
|
2222
|
+
await this.g.V(existing[0]).property("tags", JSON.stringify(Array.from(this.entityTypesCollection))).next();
|
|
2223
|
+
} else {
|
|
2224
|
+
await this.g.addV("TagCollection").property("type", "entity-types").property("tags", JSON.stringify(Array.from(this.entityTypesCollection))).next();
|
|
2225
|
+
}
|
|
2226
|
+
} catch (error) {
|
|
2227
|
+
console.error("Failed to add entity types:", error);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
async initializeTagCollections() {
|
|
2231
|
+
const collections = await this.g.V().hasLabel("TagCollection").toList();
|
|
2232
|
+
let entityTypesFromDb = [];
|
|
2233
|
+
for (const vertex of collections) {
|
|
2234
|
+
const props = vertex.properties || {};
|
|
2235
|
+
const type = this.getPropertyValue(props, "type");
|
|
2236
|
+
const tagsJson = this.getPropertyValue(props, "tags");
|
|
2237
|
+
const tags = tagsJson ? JSON.parse(tagsJson) : [];
|
|
2238
|
+
if (type === "entity-types") {
|
|
2239
|
+
entityTypesFromDb = tags;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
const { DEFAULT_ENTITY_TYPES } = await import("@semiont/ontology");
|
|
2243
|
+
this.entityTypesCollection = /* @__PURE__ */ new Set([...DEFAULT_ENTITY_TYPES, ...entityTypesFromDb]);
|
|
2244
|
+
if (entityTypesFromDb.length === 0) {
|
|
2245
|
+
await this.addEntityTypes([]);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
generateId() {
|
|
2249
|
+
return uuidv43().replace(/-/g, "").substring(0, 12);
|
|
2250
|
+
}
|
|
2251
|
+
async clearDatabase() {
|
|
2252
|
+
await this.g.V().drop().next();
|
|
2253
|
+
this.entityTypesCollection = null;
|
|
2254
|
+
console.log("Cleared JanusGraph database");
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
|
|
2258
|
+
// src/implementations/memorygraph.ts
|
|
2259
|
+
import { getResourceEntityTypes } from "@semiont/api-client";
|
|
2260
|
+
import { getEntityTypes as getEntityTypes4 } from "@semiont/ontology";
|
|
2261
|
+
import { resourceId as makeResourceId } from "@semiont/core";
|
|
2262
|
+
import { resourceUri as resourceUri2 } from "@semiont/api-client";
|
|
2263
|
+
import { v4 as uuidv44 } from "uuid";
|
|
2264
|
+
import { getBodySource as getBodySource4, getTargetSource as getTargetSource3 } from "@semiont/api-client";
|
|
2265
|
+
import { getResourceId as getResourceId3, getPrimaryRepresentation as getPrimaryRepresentation4 } from "@semiont/api-client";
|
|
2266
|
+
var MemoryGraphDatabase = class {
|
|
2267
|
+
connected = false;
|
|
2268
|
+
// In-memory storage using Maps
|
|
2269
|
+
resources = /* @__PURE__ */ new Map();
|
|
2270
|
+
annotations = /* @__PURE__ */ new Map();
|
|
2271
|
+
constructor(config = {}) {
|
|
2272
|
+
void config;
|
|
2273
|
+
}
|
|
2274
|
+
async connect() {
|
|
2275
|
+
console.log("Using in-memory graph database...");
|
|
2276
|
+
this.connected = true;
|
|
2277
|
+
}
|
|
2278
|
+
async disconnect() {
|
|
2279
|
+
this.connected = false;
|
|
2280
|
+
}
|
|
2281
|
+
isConnected() {
|
|
2282
|
+
return this.connected;
|
|
2283
|
+
}
|
|
2284
|
+
async createResource(resource) {
|
|
2285
|
+
const id = getResourceId3(resource);
|
|
2286
|
+
if (!id) {
|
|
2287
|
+
throw new Error("Resource must have an id");
|
|
2288
|
+
}
|
|
2289
|
+
this.resources.set(id, resource);
|
|
2290
|
+
return resource;
|
|
2291
|
+
}
|
|
2292
|
+
async getResource(id) {
|
|
2293
|
+
return this.resources.get(id) || null;
|
|
2294
|
+
}
|
|
2295
|
+
async updateResource(id, input) {
|
|
2296
|
+
if (Object.keys(input).length !== 1 || input.archived === void 0) {
|
|
2297
|
+
throw new Error("Resources are immutable. Only archiving is allowed.");
|
|
2298
|
+
}
|
|
2299
|
+
const doc = this.resources.get(id);
|
|
2300
|
+
if (!doc) throw new Error("Resource not found");
|
|
2301
|
+
doc.archived = input.archived;
|
|
2302
|
+
return doc;
|
|
2303
|
+
}
|
|
2304
|
+
async deleteResource(id) {
|
|
2305
|
+
this.resources.delete(id);
|
|
2306
|
+
for (const [selId, sel] of this.annotations) {
|
|
2307
|
+
if (getTargetSource3(sel.target) === id || getBodySource4(sel.body) === id) {
|
|
2308
|
+
this.annotations.delete(selId);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
async listResources(filter) {
|
|
2313
|
+
let docs = Array.from(this.resources.values());
|
|
2314
|
+
if (filter.entityTypes && filter.entityTypes.length > 0) {
|
|
2315
|
+
docs = docs.filter(
|
|
2316
|
+
(doc) => doc.entityTypes && doc.entityTypes.some((type) => filter.entityTypes.includes(type))
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
if (filter.search) {
|
|
2320
|
+
const searchLower = filter.search.toLowerCase();
|
|
2321
|
+
docs = docs.filter(
|
|
2322
|
+
(doc) => doc.name.toLowerCase().includes(searchLower)
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
const total = docs.length;
|
|
2326
|
+
const offset = filter.offset || 0;
|
|
2327
|
+
const limit = filter.limit || 20;
|
|
2328
|
+
docs = docs.slice(offset, offset + limit);
|
|
2329
|
+
return { resources: docs, total };
|
|
2330
|
+
}
|
|
2331
|
+
async searchResources(query, limit = 20) {
|
|
2332
|
+
const searchLower = query.toLowerCase();
|
|
2333
|
+
const results = Array.from(this.resources.values()).filter((doc) => doc.name.toLowerCase().includes(searchLower)).slice(0, limit);
|
|
2334
|
+
return results;
|
|
2335
|
+
}
|
|
2336
|
+
async createAnnotation(input) {
|
|
2337
|
+
const id = this.generateId();
|
|
2338
|
+
const annotation = {
|
|
2339
|
+
"@context": "http://www.w3.org/ns/anno.jsonld",
|
|
2340
|
+
"type": "Annotation",
|
|
2341
|
+
id,
|
|
2342
|
+
motivation: input.motivation,
|
|
2343
|
+
target: input.target,
|
|
2344
|
+
body: input.body,
|
|
2345
|
+
creator: input.creator,
|
|
2346
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2347
|
+
};
|
|
2348
|
+
this.annotations.set(id, annotation);
|
|
2349
|
+
console.log("Memory: Created annotation:", {
|
|
2350
|
+
id,
|
|
2351
|
+
motivation: annotation.motivation,
|
|
2352
|
+
hasSource: !!getBodySource4(annotation.body),
|
|
2353
|
+
targetSource: getTargetSource3(annotation.target)
|
|
2354
|
+
});
|
|
2355
|
+
return annotation;
|
|
2356
|
+
}
|
|
2357
|
+
async getAnnotation(id) {
|
|
2358
|
+
return this.annotations.get(id) || null;
|
|
2359
|
+
}
|
|
2360
|
+
async updateAnnotation(id, updates) {
|
|
2361
|
+
const annotation = this.annotations.get(id);
|
|
2362
|
+
if (!annotation) throw new Error("Annotation not found");
|
|
2363
|
+
const updated = {
|
|
2364
|
+
...annotation,
|
|
2365
|
+
...updates
|
|
2366
|
+
};
|
|
2367
|
+
this.annotations.set(id, updated);
|
|
2368
|
+
return updated;
|
|
2369
|
+
}
|
|
2370
|
+
async deleteAnnotation(id) {
|
|
2371
|
+
this.annotations.delete(id);
|
|
2372
|
+
}
|
|
2373
|
+
async listAnnotations(filter) {
|
|
2374
|
+
let results = Array.from(this.annotations.values());
|
|
2375
|
+
if (filter.resourceId) {
|
|
2376
|
+
const resourceIdStr = String(filter.resourceId);
|
|
2377
|
+
results = results.filter((a) => {
|
|
2378
|
+
const targetSource = getTargetSource3(a.target);
|
|
2379
|
+
return targetSource === resourceIdStr || targetSource === resourceUri2(resourceIdStr);
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
if (filter.type) {
|
|
2383
|
+
const motivation = filter.type === "highlight" ? "highlighting" : "linking";
|
|
2384
|
+
results = results.filter((a) => a.motivation === motivation);
|
|
2385
|
+
}
|
|
2386
|
+
return { annotations: results, total: results.length };
|
|
2387
|
+
}
|
|
2388
|
+
async getHighlights(resourceId) {
|
|
2389
|
+
const resourceIdStr = String(resourceId);
|
|
2390
|
+
const highlights = Array.from(this.annotations.values()).filter((sel) => {
|
|
2391
|
+
const targetSource = getTargetSource3(sel.target);
|
|
2392
|
+
return (targetSource === resourceIdStr || targetSource === resourceUri2(resourceIdStr)) && sel.motivation === "highlighting";
|
|
2393
|
+
});
|
|
2394
|
+
console.log(`Memory: getHighlights for ${resourceId} found ${highlights.length} highlights`);
|
|
2395
|
+
return highlights;
|
|
2396
|
+
}
|
|
2397
|
+
async resolveReference(annotationId, source) {
|
|
2398
|
+
const annotation = this.annotations.get(annotationId);
|
|
2399
|
+
if (!annotation) throw new Error("Annotation not found");
|
|
2400
|
+
const updated = {
|
|
2401
|
+
...annotation,
|
|
2402
|
+
body: {
|
|
2403
|
+
type: "SpecificResource",
|
|
2404
|
+
source,
|
|
2405
|
+
purpose: "linking"
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2408
|
+
this.annotations.set(annotationId, updated);
|
|
2409
|
+
return updated;
|
|
2410
|
+
}
|
|
2411
|
+
async getReferences(resourceId) {
|
|
2412
|
+
const resourceIdStr = String(resourceId);
|
|
2413
|
+
const references = Array.from(this.annotations.values()).filter((sel) => {
|
|
2414
|
+
const targetSource = getTargetSource3(sel.target);
|
|
2415
|
+
return (targetSource === resourceIdStr || targetSource === resourceUri2(resourceIdStr)) && sel.motivation === "linking";
|
|
2416
|
+
});
|
|
2417
|
+
console.log(`Memory: getReferences for ${resourceId} found ${references.length} references`);
|
|
2418
|
+
references.forEach((ref) => {
|
|
2419
|
+
console.log(" Reference:", {
|
|
2420
|
+
id: ref.id,
|
|
2421
|
+
source: getBodySource4(ref.body),
|
|
2422
|
+
entityTypes: getEntityTypes4(ref)
|
|
2423
|
+
// from body
|
|
2424
|
+
});
|
|
2425
|
+
});
|
|
2426
|
+
return references;
|
|
2427
|
+
}
|
|
2428
|
+
async getEntityReferences(resourceId, entityTypes) {
|
|
2429
|
+
const resourceIdStr = String(resourceId);
|
|
2430
|
+
let refs = Array.from(this.annotations.values()).filter((sel) => {
|
|
2431
|
+
const selEntityTypes = getEntityTypes4(sel);
|
|
2432
|
+
const targetSource = getTargetSource3(sel.target);
|
|
2433
|
+
return (targetSource === resourceIdStr || targetSource === resourceUri2(resourceIdStr)) && selEntityTypes.length > 0;
|
|
2434
|
+
});
|
|
2435
|
+
if (entityTypes && entityTypes.length > 0) {
|
|
2436
|
+
refs = refs.filter((sel) => {
|
|
2437
|
+
const selEntityTypes = getEntityTypes4(sel);
|
|
2438
|
+
return selEntityTypes.some((type) => entityTypes.includes(type));
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
return refs;
|
|
2442
|
+
}
|
|
2443
|
+
async getResourceAnnotations(resourceId) {
|
|
2444
|
+
const resourceIdStr = String(resourceId);
|
|
2445
|
+
return Array.from(this.annotations.values()).filter((sel) => {
|
|
2446
|
+
const targetSource = getTargetSource3(sel.target);
|
|
2447
|
+
return targetSource === resourceIdStr || targetSource === resourceUri2(resourceIdStr);
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
async getResourceReferencedBy(resourceUri3, _motivation) {
|
|
2451
|
+
return Array.from(this.annotations.values()).filter((sel) => getBodySource4(sel.body) === resourceUri3);
|
|
2452
|
+
}
|
|
2453
|
+
async getResourceConnections(resourceId) {
|
|
2454
|
+
const connections = [];
|
|
2455
|
+
const refs = await this.getReferences(resourceId);
|
|
2456
|
+
for (const ref of refs) {
|
|
2457
|
+
const bodySource = getBodySource4(ref.body);
|
|
2458
|
+
if (bodySource) {
|
|
2459
|
+
const targetDoc = await this.getResource(resourceUri2(bodySource));
|
|
2460
|
+
if (targetDoc) {
|
|
2461
|
+
const reverseRefs = await this.getReferences(makeResourceId(bodySource));
|
|
2462
|
+
const resourceIdStr = String(resourceId);
|
|
2463
|
+
const bidirectional = reverseRefs.some((r) => {
|
|
2464
|
+
const reverseBodySource = getBodySource4(r.body);
|
|
2465
|
+
return reverseBodySource === resourceIdStr || reverseBodySource === resourceUri2(resourceIdStr);
|
|
2466
|
+
});
|
|
2467
|
+
connections.push({
|
|
2468
|
+
targetResource: targetDoc,
|
|
2469
|
+
annotations: [ref],
|
|
2470
|
+
bidirectional
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return connections;
|
|
2476
|
+
}
|
|
2477
|
+
async findPath(fromResourceId, toResourceId, maxDepth = 5) {
|
|
2478
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2479
|
+
const queue = [];
|
|
2480
|
+
const fromDoc = await this.getResource(resourceUri2(fromResourceId));
|
|
2481
|
+
if (!fromDoc) return [];
|
|
2482
|
+
queue.push({ docId: fromResourceId, path: [fromDoc], sels: [] });
|
|
2483
|
+
visited.add(fromResourceId);
|
|
2484
|
+
const paths = [];
|
|
2485
|
+
while (queue.length > 0 && paths.length < 10) {
|
|
2486
|
+
const { docId, path, sels } = queue.shift();
|
|
2487
|
+
if (path.length > maxDepth) continue;
|
|
2488
|
+
if (docId === toResourceId) {
|
|
2489
|
+
paths.push({ resources: path, annotations: sels });
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
const connections = await this.getResourceConnections(makeResourceId(docId));
|
|
2493
|
+
for (const conn of connections) {
|
|
2494
|
+
const targetId = getResourceId3(conn.targetResource);
|
|
2495
|
+
if (targetId && !visited.has(targetId)) {
|
|
2496
|
+
visited.add(targetId);
|
|
2497
|
+
queue.push({
|
|
2498
|
+
docId: targetId,
|
|
2499
|
+
path: [...path, conn.targetResource],
|
|
2500
|
+
sels: [...sels, ...conn.annotations]
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return paths;
|
|
2506
|
+
}
|
|
2507
|
+
async getEntityTypeStats() {
|
|
2508
|
+
const typeCounts = /* @__PURE__ */ new Map();
|
|
2509
|
+
for (const doc of this.resources.values()) {
|
|
2510
|
+
const types = getResourceEntityTypes(doc);
|
|
2511
|
+
for (const type of types) {
|
|
2512
|
+
typeCounts.set(type, (typeCounts.get(type) || 0) + 1);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
return Array.from(typeCounts.entries()).map(([type, count]) => ({
|
|
2516
|
+
type,
|
|
2517
|
+
count
|
|
2518
|
+
}));
|
|
2519
|
+
}
|
|
2520
|
+
async getStats() {
|
|
2521
|
+
const entityTypes = {};
|
|
2522
|
+
const contentTypes = {};
|
|
2523
|
+
for (const doc of this.resources.values()) {
|
|
2524
|
+
for (const type of doc.entityTypes || []) {
|
|
2525
|
+
entityTypes[type] = (entityTypes[type] || 0) + 1;
|
|
2526
|
+
}
|
|
2527
|
+
const primaryRep = getPrimaryRepresentation4(doc);
|
|
2528
|
+
if (primaryRep?.mediaType) {
|
|
2529
|
+
contentTypes[primaryRep.mediaType] = (contentTypes[primaryRep.mediaType] || 0) + 1;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
const annotations = Array.from(this.annotations.values());
|
|
2533
|
+
const highlightCount = annotations.filter((a) => a.motivation === "highlighting").length;
|
|
2534
|
+
const referenceCount = annotations.filter((a) => a.motivation === "linking").length;
|
|
2535
|
+
const entityReferenceCount = annotations.filter((a) => a.motivation === "linking" && getEntityTypes4(a).length > 0).length;
|
|
2536
|
+
return {
|
|
2537
|
+
resourceCount: this.resources.size,
|
|
2538
|
+
annotationCount: this.annotations.size,
|
|
2539
|
+
highlightCount,
|
|
2540
|
+
referenceCount,
|
|
2541
|
+
entityReferenceCount,
|
|
2542
|
+
entityTypes,
|
|
2543
|
+
contentTypes
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
async createAnnotations(inputs) {
|
|
2547
|
+
const results = [];
|
|
2548
|
+
for (const input of inputs) {
|
|
2549
|
+
results.push(await this.createAnnotation(input));
|
|
2550
|
+
}
|
|
2551
|
+
return results;
|
|
2552
|
+
}
|
|
2553
|
+
async resolveReferences(inputs) {
|
|
2554
|
+
const results = [];
|
|
2555
|
+
for (const input of inputs) {
|
|
2556
|
+
results.push(await this.resolveReference(input.annotationId, input.source));
|
|
2557
|
+
}
|
|
2558
|
+
return results;
|
|
2559
|
+
}
|
|
2560
|
+
async detectAnnotations(_resourceId) {
|
|
2561
|
+
return [];
|
|
2562
|
+
}
|
|
2563
|
+
// Tag Collections - stored as special vertices in the graph
|
|
2564
|
+
entityTypesCollection = null;
|
|
2565
|
+
async getEntityTypes() {
|
|
2566
|
+
if (this.entityTypesCollection === null) {
|
|
2567
|
+
await this.initializeTagCollections();
|
|
2568
|
+
}
|
|
2569
|
+
return Array.from(this.entityTypesCollection).sort();
|
|
2570
|
+
}
|
|
2571
|
+
async addEntityType(tag) {
|
|
2572
|
+
if (this.entityTypesCollection === null) {
|
|
2573
|
+
await this.initializeTagCollections();
|
|
2574
|
+
}
|
|
2575
|
+
this.entityTypesCollection.add(tag);
|
|
2576
|
+
}
|
|
2577
|
+
async addEntityTypes(tags) {
|
|
2578
|
+
if (this.entityTypesCollection === null) {
|
|
2579
|
+
await this.initializeTagCollections();
|
|
2580
|
+
}
|
|
2581
|
+
tags.forEach((tag) => this.entityTypesCollection.add(tag));
|
|
2582
|
+
}
|
|
2583
|
+
async initializeTagCollections() {
|
|
2584
|
+
if (this.entityTypesCollection === null) {
|
|
2585
|
+
const { DEFAULT_ENTITY_TYPES } = await import("@semiont/ontology");
|
|
2586
|
+
this.entityTypesCollection = new Set(DEFAULT_ENTITY_TYPES);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
generateId() {
|
|
2590
|
+
return uuidv44().replace(/-/g, "").substring(0, 12);
|
|
2591
|
+
}
|
|
2592
|
+
async clearDatabase() {
|
|
2593
|
+
this.resources.clear();
|
|
2594
|
+
this.annotations.clear();
|
|
2595
|
+
this.entityTypesCollection = null;
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
|
|
2599
|
+
// src/factory.ts
|
|
2600
|
+
var graphDatabaseInstance = null;
|
|
2601
|
+
function createGraphDatabase(config, envConfig) {
|
|
2602
|
+
switch (config.type) {
|
|
2603
|
+
case "neptune": {
|
|
2604
|
+
const neptuneConfig = {};
|
|
2605
|
+
if (config.neptuneEndpoint !== void 0) neptuneConfig.endpoint = config.neptuneEndpoint;
|
|
2606
|
+
if (config.neptunePort !== void 0) neptuneConfig.port = config.neptunePort;
|
|
2607
|
+
if (config.neptuneRegion !== void 0) neptuneConfig.region = config.neptuneRegion;
|
|
2608
|
+
return new NeptuneGraphDatabase(neptuneConfig);
|
|
2609
|
+
}
|
|
2610
|
+
case "neo4j": {
|
|
2611
|
+
const neo4jConfig = {};
|
|
2612
|
+
if (config.neo4jUri !== void 0) neo4jConfig.uri = config.neo4jUri;
|
|
2613
|
+
if (config.neo4jUsername !== void 0) neo4jConfig.username = config.neo4jUsername;
|
|
2614
|
+
if (config.neo4jPassword !== void 0) neo4jConfig.password = config.neo4jPassword;
|
|
2615
|
+
if (config.neo4jDatabase !== void 0) neo4jConfig.database = config.neo4jDatabase;
|
|
2616
|
+
return new Neo4jGraphDatabase(neo4jConfig);
|
|
2617
|
+
}
|
|
2618
|
+
case "janusgraph": {
|
|
2619
|
+
const janusConfig = {};
|
|
2620
|
+
if (config.janusHost !== void 0) janusConfig.host = config.janusHost;
|
|
2621
|
+
if (config.janusPort !== void 0) janusConfig.port = config.janusPort;
|
|
2622
|
+
if (config.janusStorageBackend !== void 0) janusConfig.storageBackend = config.janusStorageBackend;
|
|
2623
|
+
if (config.janusIndexBackend !== void 0) janusConfig.indexBackend = config.janusIndexBackend;
|
|
2624
|
+
return new JanusGraphDatabase(janusConfig, envConfig);
|
|
2625
|
+
}
|
|
2626
|
+
case "memory":
|
|
2627
|
+
return new MemoryGraphDatabase({});
|
|
2628
|
+
default:
|
|
2629
|
+
throw new Error(`Unsupported graph database type: ${config.type}`);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
function evaluateEnvVar(value) {
|
|
2633
|
+
if (!value) return void 0;
|
|
2634
|
+
return value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
2635
|
+
const envValue = process.env[varName];
|
|
2636
|
+
if (!envValue) {
|
|
2637
|
+
throw new Error(`Environment variable ${varName} is not set. Referenced in configuration as ${match}`);
|
|
2638
|
+
}
|
|
2639
|
+
return envValue;
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
async function getGraphDatabase(envConfig) {
|
|
2643
|
+
if (!graphDatabaseInstance) {
|
|
2644
|
+
const graphConfig = envConfig.services.graph;
|
|
2645
|
+
const config = {
|
|
2646
|
+
type: graphConfig.type
|
|
2647
|
+
};
|
|
2648
|
+
if (graphConfig.type === "janusgraph") {
|
|
2649
|
+
if (graphConfig.host) {
|
|
2650
|
+
config.janusHost = graphConfig.host;
|
|
2651
|
+
}
|
|
2652
|
+
if (graphConfig.port) {
|
|
2653
|
+
config.janusPort = graphConfig.port;
|
|
2654
|
+
}
|
|
2655
|
+
if (graphConfig.storage) {
|
|
2656
|
+
config.janusStorageBackend = graphConfig.storage;
|
|
2657
|
+
}
|
|
2658
|
+
if (graphConfig.index && graphConfig.index !== "none") {
|
|
2659
|
+
config.janusIndexBackend = graphConfig.index;
|
|
2660
|
+
}
|
|
2661
|
+
} else if (graphConfig.type === "neptune") {
|
|
2662
|
+
if (graphConfig.endpoint) {
|
|
2663
|
+
config.neptuneEndpoint = graphConfig.endpoint;
|
|
2664
|
+
}
|
|
2665
|
+
if (graphConfig.port) {
|
|
2666
|
+
config.neptunePort = graphConfig.port;
|
|
2667
|
+
}
|
|
2668
|
+
if (graphConfig.region) {
|
|
2669
|
+
config.neptuneRegion = graphConfig.region;
|
|
2670
|
+
}
|
|
2671
|
+
} else if (graphConfig.type === "neo4j") {
|
|
2672
|
+
if (graphConfig.uri) {
|
|
2673
|
+
config.neo4jUri = evaluateEnvVar(graphConfig.uri);
|
|
2674
|
+
}
|
|
2675
|
+
if (graphConfig.username) {
|
|
2676
|
+
config.neo4jUsername = evaluateEnvVar(graphConfig.username);
|
|
2677
|
+
}
|
|
2678
|
+
if (graphConfig.password) {
|
|
2679
|
+
config.neo4jPassword = evaluateEnvVar(graphConfig.password);
|
|
2680
|
+
}
|
|
2681
|
+
if (graphConfig.database) {
|
|
2682
|
+
config.neo4jDatabase = evaluateEnvVar(graphConfig.database);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
graphDatabaseInstance = createGraphDatabase(config, envConfig);
|
|
2686
|
+
await graphDatabaseInstance.connect();
|
|
2687
|
+
}
|
|
2688
|
+
if (!graphDatabaseInstance.isConnected()) {
|
|
2689
|
+
await graphDatabaseInstance.connect();
|
|
2690
|
+
}
|
|
2691
|
+
return graphDatabaseInstance;
|
|
2692
|
+
}
|
|
2693
|
+
async function closeGraphDatabase() {
|
|
2694
|
+
if (graphDatabaseInstance) {
|
|
2695
|
+
await graphDatabaseInstance.disconnect();
|
|
2696
|
+
graphDatabaseInstance = null;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
export {
|
|
2700
|
+
JanusGraphDatabase,
|
|
2701
|
+
MemoryGraphDatabase,
|
|
2702
|
+
Neo4jGraphDatabase,
|
|
2703
|
+
NeptuneGraphDatabase,
|
|
2704
|
+
closeGraphDatabase,
|
|
2705
|
+
createGraphDatabase,
|
|
2706
|
+
getGraphDatabase
|
|
2707
|
+
};
|
|
2708
|
+
//# sourceMappingURL=index.js.map
|