@graphty/graph-io 0.3.17 → 0.3.18

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.
Files changed (91) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +29 -3
  3. package/dist/chunks/{escape-scjHxjpr.js → escape-D-gZWO26.js} +3 -2
  4. package/dist/chunks/{escape-scjHxjpr.js.map → escape-D-gZWO26.js.map} +1 -1
  5. package/dist/chunks/{importer-Xg07gk7p.js → importer-Br_QeAeE.js} +4 -3
  6. package/dist/chunks/{importer-Xg07gk7p.js.map → importer-Br_QeAeE.js.map} +1 -1
  7. package/dist/chunks/{importer-ruMWWvVs.js → importer-DHagxvDD.js} +4 -3
  8. package/dist/chunks/{importer-ruMWWvVs.js.map → importer-DHagxvDD.js.map} +1 -1
  9. package/dist/chunks/{importer-B6rKRxzv.js → importer-Du5crN9l.js} +509 -25
  10. package/dist/chunks/importer-Du5crN9l.js.map +1 -0
  11. package/dist/chunks/importer-aNJfe0qu.js +1614 -0
  12. package/dist/chunks/importer-aNJfe0qu.js.map +1 -0
  13. package/dist/chunks/{importer-BmOl9gLW.js → importer-d0uQxFp6.js} +4 -3
  14. package/dist/chunks/{importer-BmOl9gLW.js.map → importer-d0uQxFp6.js.map} +1 -1
  15. package/dist/chunks/ontology-BnrJ4I98.js +113 -0
  16. package/dist/chunks/ontology-BnrJ4I98.js.map +1 -0
  17. package/dist/chunks/{records-DSpbTE5s.js → records-Bk9jgodz.js} +2 -2
  18. package/dist/chunks/{records-DSpbTE5s.js.map → records-Bk9jgodz.js.map} +1 -1
  19. package/dist/chunks/{writer-DHHfHn11.js → report-BOk0p5y8.js} +97 -919
  20. package/dist/chunks/report-BOk0p5y8.js.map +1 -0
  21. package/dist/chunks/writer-GAdltGmC.js +827 -0
  22. package/dist/chunks/writer-GAdltGmC.js.map +1 -0
  23. package/dist/csv.js +4 -3
  24. package/dist/csv.js.map +1 -1
  25. package/dist/dot.js +1 -1
  26. package/dist/gexf.js +3 -2
  27. package/dist/gexf.js.map +1 -1
  28. package/dist/gml.js +3 -2
  29. package/dist/gml.js.map +1 -1
  30. package/dist/graph-io.js +160 -141
  31. package/dist/graph-io.js.map +1 -1
  32. package/dist/graphml.js +1 -1
  33. package/dist/json.js +1 -1
  34. package/dist/neo4j.js +4 -3
  35. package/dist/neo4j.js.map +1 -1
  36. package/dist/obo.d.ts +1 -0
  37. package/dist/obo.js +6 -0
  38. package/dist/obo.js.map +1 -0
  39. package/dist/pajek.js +1 -1
  40. package/dist/src/common/ontology.d.ts +59 -0
  41. package/dist/src/common/ontology.d.ts.map +1 -0
  42. package/dist/src/common/ontology.js +147 -0
  43. package/dist/src/common/ontology.js.map +1 -0
  44. package/dist/src/formats/json/dialect.d.ts +8 -6
  45. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  46. package/dist/src/formats/json/dialect.js +26 -3
  47. package/dist/src/formats/json/dialect.js.map +1 -1
  48. package/dist/src/formats/json/importer.d.ts +324 -7
  49. package/dist/src/formats/json/importer.d.ts.map +1 -1
  50. package/dist/src/formats/json/importer.js +153 -22
  51. package/dist/src/formats/json/importer.js.map +1 -1
  52. package/dist/src/formats/json/obographs.d.ts +21 -0
  53. package/dist/src/formats/json/obographs.d.ts.map +1 -0
  54. package/dist/src/formats/json/obographs.js +476 -0
  55. package/dist/src/formats/json/obographs.js.map +1 -0
  56. package/dist/src/formats/obo/importer.d.ts +90 -0
  57. package/dist/src/formats/obo/importer.d.ts.map +1 -0
  58. package/dist/src/formats/obo/importer.js +1248 -0
  59. package/dist/src/formats/obo/importer.js.map +1 -0
  60. package/dist/src/formats/obo/index.d.ts +7 -0
  61. package/dist/src/formats/obo/index.d.ts.map +1 -0
  62. package/dist/src/formats/obo/index.js +7 -0
  63. package/dist/src/formats/obo/index.js.map +1 -0
  64. package/dist/src/formats/obo/syntax.d.ts +121 -0
  65. package/dist/src/formats/obo/syntax.d.ts.map +1 -0
  66. package/dist/src/formats/obo/syntax.js +424 -0
  67. package/dist/src/formats/obo/syntax.js.map +1 -0
  68. package/dist/src/index.d.ts +1 -0
  69. package/dist/src/index.d.ts.map +1 -1
  70. package/dist/src/index.js +1 -0
  71. package/dist/src/index.js.map +1 -1
  72. package/dist/src/registry.d.ts.map +1 -1
  73. package/dist/src/registry.js +3 -1
  74. package/dist/src/registry.js.map +1 -1
  75. package/dist/src/sniff.d.ts +1 -1
  76. package/dist/src/sniff.d.ts.map +1 -1
  77. package/dist/src/sniff.js +23 -3
  78. package/dist/src/sniff.js.map +1 -1
  79. package/package.json +6 -1
  80. package/src/common/ontology.ts +169 -0
  81. package/src/formats/json/dialect.ts +37 -7
  82. package/src/formats/json/importer.ts +204 -27
  83. package/src/formats/json/obographs.ts +563 -0
  84. package/src/formats/obo/importer.ts +1695 -0
  85. package/src/formats/obo/index.ts +7 -0
  86. package/src/formats/obo/syntax.ts +466 -0
  87. package/src/index.ts +1 -0
  88. package/src/registry.ts +3 -1
  89. package/src/sniff.ts +35 -5
  90. package/dist/chunks/importer-B6rKRxzv.js.map +0 -1
  91. package/dist/chunks/writer-DHHfHn11.js.map +0 -1
@@ -0,0 +1,563 @@
1
+ /**
2
+ * The `obographs` dialect of the JSON importer (design sections 1.6 and 4.6): OBO Graphs JSON,
3
+ * the form in which the Gene Ontology and every OBO Foundry ontology publish a ready-made graph
4
+ * (`{ "graphs": [{ "nodes": [...], "edges": [{ "sub", "pred", "obj" }] }] }`).
5
+ *
6
+ * Nodes and their `meta` fill the OBO column vocabulary of src/common/ontology.ts, the same
7
+ * columns the OBO importer gives the `.obo` file of the same ontology; ids are compacted from IRIs
8
+ * to the identifiers the `.obo` writes (`oboIds: "curie"`, the default), and a relation is named by
9
+ * its shorthand (`part_of`, not `http://purl.obolibrary.org/obo/BFO_0000050`). PROPERTY nodes and
10
+ * the edges between properties are metadata unless `typedefs: "nodes"`, as `[Typedef]` frames are.
11
+ * The axiom arrays and the graph and document metadata are kept verbatim in
12
+ * `meta.extra.obographs`.
13
+ */
14
+
15
+ import { type ColumnHandle, INVALID_INDEX, type NodeId } from "@graphty/graph-format";
16
+
17
+ import { declareResolved } from "../../common/attributes.js";
18
+ import {
19
+ compactOboIri,
20
+ OBO_NODE_COLUMNS,
21
+ oboColumnDecl,
22
+ OBOGRAPHS_PREDICATE_TAGS,
23
+ PLACEHOLDER_COLUMN,
24
+ synonymScopeOf,
25
+ } from "../../common/ontology.js";
26
+ import { hasKey, isJsonObject } from "./dialect.js";
27
+ import { chosenGraph, type ImportContext, JSON_ISSUE, type JsonRecord } from "./importer.js";
28
+
29
+ /** The node keys the schema defines; any other key goes to `obo.unrecognized`. */
30
+ const NODE_KEYS: ReadonlySet<string> = new Set(["id", "lbl", "type", "propertyType", "meta"]);
31
+
32
+ /** The `meta` keys mapped onto columns; any other key goes to `obo.unrecognized` as `meta.<key>`. */
33
+ const META_KEYS: ReadonlySet<string> = new Set([
34
+ "definition",
35
+ "comments",
36
+ "subsets",
37
+ "xrefs",
38
+ "synonyms",
39
+ "basicPropertyValues",
40
+ "deprecated",
41
+ ]);
42
+
43
+ /** The OBO frame type of an OBO Graphs node type. */
44
+ const FRAME_TYPES: Readonly<Record<string, string>> = Object.freeze({
45
+ CLASS: "Term",
46
+ INDIVIDUAL: "Instance",
47
+ PROPERTY: "Typedef",
48
+ });
49
+
50
+ /** The OBO name of the predicates OBO Graphs writes without an IRI. */
51
+ const BUILTIN_PREDICATES: Readonly<Record<string, string>> = Object.freeze({
52
+ is_a: "is_a",
53
+ subPropertyOf: "is_a",
54
+ type: "instance_of",
55
+ inverseOf: "inverse_of",
56
+ });
57
+
58
+ /** Predicates that relate two properties (metadata under typedefs "metadata"). */
59
+ const PROPERTY_PREDICATES: ReadonlySet<string> = new Set(["subPropertyOf", "inverseOf"]);
60
+
61
+ /** Writes the OBO vocabulary columns, declaring each on first use. */
62
+ class ColumnWriter {
63
+ private readonly ctx: ImportContext;
64
+
65
+ private readonly handles = new Map<string, ColumnHandle>();
66
+
67
+ /**
68
+ * Create a writer.
69
+ * @param ctx - the import context
70
+ */
71
+ constructor(ctx: ImportContext) {
72
+ this.ctx = ctx;
73
+ }
74
+
75
+ /**
76
+ * Write a node cell of the vocabulary.
77
+ * @param name - the column
78
+ * @param row - the node index
79
+ * @param value - the value
80
+ */
81
+ node(name: string, row: number, value: unknown): void {
82
+ this.ctx.sink.setNodeValue(this.handle("node", name), row, value);
83
+ }
84
+
85
+ /**
86
+ * Write an edge cell of the vocabulary.
87
+ * @param name - the column
88
+ * @param row - the edge index
89
+ * @param value - the value
90
+ */
91
+ edge(name: string, row: number, value: unknown): void {
92
+ this.ctx.sink.setEdgeValue(this.handle("edge", name), row, value);
93
+ }
94
+
95
+ /**
96
+ * The handle of a column, declared on first use.
97
+ * @param domain - node or edge
98
+ * @param name - the column
99
+ * @returns the handle
100
+ */
101
+ private handle(domain: "node" | "edge", name: string): ColumnHandle {
102
+ const key = `${domain}:${name}`;
103
+ const cached = this.handles.get(key);
104
+ if (cached !== undefined) {
105
+ return cached;
106
+ }
107
+ const { handle } = declareResolved(this.ctx.sink, domain, oboColumnDecl(domain, name), this.ctx.report);
108
+ this.handles.set(key, handle);
109
+ return handle;
110
+ }
111
+ }
112
+
113
+ /** One import's view of the graph's ids and properties. */
114
+ interface Vocabulary {
115
+ /** An IRI as the node id or relation name the snapshot uses. */
116
+ readonly id: (iri: string) => string;
117
+ /** A predicate as the relation name the edge column holds. */
118
+ readonly relation: (pred: string) => string;
119
+ /** Whether an IRI names a PROPERTY node. */
120
+ readonly isProperty: (iri: string) => boolean;
121
+ /** The PROPERTY nodes kept as metadata, by IRI. */
122
+ readonly properties: Record<string, unknown>;
123
+ }
124
+
125
+ /**
126
+ * The id rule and the property table of one graph: PROPERTY nodes are found first, so an edge's
127
+ * predicate is named by the shorthand of the property it names wherever that property stands.
128
+ * @param nodes - the graph's node records
129
+ * @param oboIds - "curie" or "iri"
130
+ * @returns the vocabulary
131
+ */
132
+ function vocabularyOf(nodes: readonly unknown[], oboIds: "curie" | "iri"): Vocabulary {
133
+ const shorthands = new Map<string, string>();
134
+ const propertyIds = new Set<string>();
135
+ for (const node of nodes) {
136
+ if (!isJsonObject(node) || typeof node.id !== "string" || node.type !== "PROPERTY") {
137
+ continue;
138
+ }
139
+ propertyIds.add(node.id);
140
+ const shorthand = basicValues(node.meta).find((pv) => OBOGRAPHS_PREDICATE_TAGS.get(pv.pred) === "shorthand");
141
+ if (shorthand !== undefined) {
142
+ shorthands.set(node.id, shorthand.val);
143
+ }
144
+ }
145
+ const compact = (iri: string): string => (oboIds === "curie" ? compactOboIri(iri) : iri);
146
+ return {
147
+ id: (iri) => (oboIds === "curie" ? (shorthands.get(iri) ?? compactOboIri(iri)) : iri),
148
+ relation: (pred) => BUILTIN_PREDICATES[pred] ?? shorthands.get(pred) ?? compact(pred),
149
+ isProperty: (iri) => propertyIds.has(iri),
150
+ properties: {},
151
+ };
152
+ }
153
+
154
+ /** One `basicPropertyValues` entry. */
155
+ interface PropertyValue {
156
+ readonly pred: string;
157
+ readonly val: string;
158
+ }
159
+
160
+ /**
161
+ * The well-formed `basicPropertyValues` of a node's meta.
162
+ * @param meta - the node's meta, or anything
163
+ * @returns the entries with a string pred and val
164
+ */
165
+ function basicValues(meta: unknown): PropertyValue[] {
166
+ if (!isJsonObject(meta) || !Array.isArray(meta.basicPropertyValues)) {
167
+ return [];
168
+ }
169
+ return meta.basicPropertyValues.filter(
170
+ (pv): pv is PropertyValue => isJsonObject(pv) && typeof pv.pred === "string" && typeof pv.val === "string",
171
+ );
172
+ }
173
+
174
+ /**
175
+ * The strings of an array, or of an array of `{ val }` records.
176
+ * @param value - the array, or anything
177
+ * @returns the strings
178
+ */
179
+ function strings(value: unknown): string[] {
180
+ if (!Array.isArray(value)) {
181
+ return [];
182
+ }
183
+ return value.flatMap((item: unknown) => {
184
+ if (typeof item === "string") {
185
+ return [item];
186
+ }
187
+ return isJsonObject(item) && typeof item.val === "string" ? [item.val] : [];
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Read the chosen graph of an OBO Graphs document into the sink.
193
+ * @param ctx - the import context
194
+ * @param root - the document
195
+ */
196
+ export function importObographs(ctx: ImportContext, root: JsonRecord): void {
197
+ const graph = chosenGraph(ctx, root, "OBO Graphs");
198
+ const nodes = sectionOf(ctx, graph, "nodes");
199
+ const edges = sectionOf(ctx, graph, "edges");
200
+ const vocabulary = vocabularyOf(nodes, ctx.json.oboIds);
201
+ const columns = new ColumnWriter(ctx);
202
+ ctx.reportNodeIdFrom("obographs", "the node ids");
203
+ ctx.setHeader(true);
204
+ ctx.sink.reserve(nodes.length, edges.length);
205
+ for (let i = 0; i < nodes.length; i++) {
206
+ readNode(ctx, columns, vocabulary, nodes[i], `nodes[${i}]`);
207
+ }
208
+ const propertyEdges = readEdges(ctx, columns, vocabulary, edges);
209
+ const graphMeta: JsonRecord = {};
210
+ for (const key of Object.keys(graph)) {
211
+ if (key !== "nodes" && key !== "edges") {
212
+ graphMeta[key] = graph[key];
213
+ }
214
+ }
215
+ const document: JsonRecord = {};
216
+ for (const key of Object.keys(root)) {
217
+ if (key !== "graphs") {
218
+ document[key] = root[key];
219
+ }
220
+ }
221
+ const label = typeof graph.lbl === "string" ? graph.lbl : null;
222
+ const name = label ?? (typeof graph.id === "string" ? graph.id : null);
223
+ ctx.sink.setMeta({
224
+ sourceFormat: "json",
225
+ name,
226
+ extra: {
227
+ json: { dialect: "obographs" },
228
+ obographs: {
229
+ ids: ctx.json.oboIds,
230
+ graph: graphMeta,
231
+ document,
232
+ properties: vocabulary.properties,
233
+ propertyEdges,
234
+ },
235
+ },
236
+ });
237
+ }
238
+
239
+ /**
240
+ * A `nodes` or `edges` section: an array, absent (empty), or a fatal E_JSON_SHAPE.
241
+ * @param ctx - the context
242
+ * @param graph - the graph
243
+ * @param key - the section
244
+ * @returns the elements
245
+ */
246
+ function sectionOf(ctx: ImportContext, graph: JsonRecord, key: string): readonly unknown[] {
247
+ const value = graph[key];
248
+ if (value === undefined || value === null) {
249
+ return [];
250
+ }
251
+ if (!Array.isArray(value)) {
252
+ return ctx.report.fail(JSON_ISSUE.SHAPE, `an OBO Graphs graph's ${key} must be an array`, { element: key });
253
+ }
254
+ return value;
255
+ }
256
+
257
+ /**
258
+ * Read one node: a PROPERTY into the metadata (typedefs "metadata"), anything else into the sink
259
+ * with its OBO columns.
260
+ * @param ctx - the context
261
+ * @param columns - the column writer
262
+ * @param vocabulary - the graph's vocabulary
263
+ * @param record - the node record
264
+ * @param element - its name in messages
265
+ */
266
+ function readNode(
267
+ ctx: ImportContext,
268
+ columns: ColumnWriter,
269
+ vocabulary: Vocabulary,
270
+ record: unknown,
271
+ element: string,
272
+ ): void {
273
+ if (!isJsonObject(record)) {
274
+ ctx.badElement("node", element);
275
+ return;
276
+ }
277
+ if (typeof record.id !== "string") {
278
+ if (record.id === undefined) {
279
+ ctx.coerceId(undefined, element);
280
+ } else {
281
+ badValue(ctx, `${element}.id must be a string; the node is skipped`);
282
+ }
283
+ ctx.countSkipped("node");
284
+ return;
285
+ }
286
+ if (record.type === "PROPERTY" && ctx.json.typedefs === "metadata") {
287
+ const shorthand = vocabulary.id(record.id) === record.id ? undefined : vocabulary.id(record.id);
288
+ const entry: JsonRecord = {};
289
+ for (const [key, value] of Object.entries({
290
+ lbl: record.lbl,
291
+ propertyType: record.propertyType,
292
+ shorthand,
293
+ meta: record.meta,
294
+ })) {
295
+ if (value !== undefined) {
296
+ entry[key] = value;
297
+ }
298
+ }
299
+ vocabulary.properties[record.id] = entry;
300
+ return;
301
+ }
302
+ const id = ctx.coerceId(vocabulary.id(record.id), element);
303
+ const row = id === null ? -1 : ctx.pushNode(id, element);
304
+ if (row < 0) {
305
+ return;
306
+ }
307
+ try {
308
+ writeNode(ctx, columns, vocabulary, record, row, element);
309
+ } catch (err) {
310
+ ctx.report.recordError(err, { element: record.id });
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Write a node's fields and meta onto the OBO columns.
316
+ * @param ctx - the context
317
+ * @param columns - the column writer
318
+ * @param vocabulary - the graph's vocabulary
319
+ * @param record - the node record
320
+ * @param row - the node index
321
+ * @param element - its name in messages
322
+ */
323
+ function writeNode(
324
+ ctx: ImportContext,
325
+ columns: ColumnWriter,
326
+ vocabulary: Vocabulary,
327
+ record: JsonRecord,
328
+ row: number,
329
+ element: string,
330
+ ): void {
331
+ const unrecognized: JsonRecord = {};
332
+ const text = (key: string, column: string): void => {
333
+ const value = record[key];
334
+ if (typeof value === "string") {
335
+ columns.node(column, row, column === "type" ? (FRAME_TYPES[value] ?? value) : value);
336
+ } else if (value !== undefined && value !== null) {
337
+ badValue(ctx, `${element}.${key} must be a string`);
338
+ }
339
+ };
340
+ text("type", "type");
341
+ text("lbl", "name");
342
+ text("propertyType", "propertyType");
343
+ for (const key of Object.keys(record)) {
344
+ if (!NODE_KEYS.has(key)) {
345
+ unrecognized[key] = record[key];
346
+ }
347
+ }
348
+ const { meta } = record;
349
+ if (isJsonObject(meta)) {
350
+ writeMeta(columns, vocabulary, meta, row, unrecognized);
351
+ } else if (meta !== undefined && meta !== null) {
352
+ badValue(ctx, `${element}.meta must be an object`);
353
+ }
354
+ if (Object.keys(unrecognized).length > 0) {
355
+ columns.node("obo.unrecognized", row, unrecognized);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Write a node's `meta` onto the OBO columns (design 4.6): definition, comments, subsets, xrefs,
361
+ * synonyms, deprecated, and the basicPropertyValues mapped back to the OBO tag they came from.
362
+ * @param columns - the column writer
363
+ * @param vocabulary - the graph's vocabulary
364
+ * @param meta - the meta record
365
+ * @param row - the node index
366
+ * @param unrecognized - where keys without a column go
367
+ */
368
+ function writeMeta(
369
+ columns: ColumnWriter,
370
+ vocabulary: Vocabulary,
371
+ meta: JsonRecord,
372
+ row: number,
373
+ unrecognized: JsonRecord,
374
+ ): void {
375
+ const { definition } = meta;
376
+ if (isJsonObject(definition) && typeof definition.val === "string") {
377
+ columns.node("def", row, definition.val);
378
+ columns.node("def.xrefs", row, strings(definition.xrefs));
379
+ }
380
+ const comments = strings(meta.comments);
381
+ if (comments.length > 0) {
382
+ columns.node("comment", row, comments.join("\n"));
383
+ }
384
+ const subsets = strings(meta.subsets).map((s) => vocabulary.id(s));
385
+ if (subsets.length > 0) {
386
+ columns.node("subset", row, subsets);
387
+ }
388
+ const xrefs = strings(meta.xrefs);
389
+ if (xrefs.length > 0) {
390
+ columns.node("xref", row, xrefs);
391
+ }
392
+ if (Array.isArray(meta.synonyms) && meta.synonyms.length > 0) {
393
+ columns.node(
394
+ "synonym",
395
+ row,
396
+ meta.synonyms.filter(isJsonObject).map((s) => ({
397
+ text: typeof s.val === "string" ? s.val : null,
398
+ scope: typeof s.pred === "string" ? synonymScopeOf(s.pred) : null,
399
+ type: typeof s.synonymType === "string" ? vocabulary.id(s.synonymType) : null,
400
+ xrefs: strings(s.xrefs),
401
+ })),
402
+ );
403
+ }
404
+ if (typeof meta.deprecated === "boolean") {
405
+ columns.node("is_obsolete", row, meta.deprecated);
406
+ }
407
+ writePropertyValues(columns, vocabulary, basicValues(meta), row);
408
+ for (const key of Object.keys(meta)) {
409
+ if (!META_KEYS.has(key)) {
410
+ unrecognized[`meta.${key}`] = meta[key];
411
+ }
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Write the basicPropertyValues: the ones an OBO tag maps to onto that tag's column (lists for
417
+ * alt_id, replaced_by, consider), the rest into `property_value`.
418
+ * @param columns - the column writer
419
+ * @param vocabulary - the graph's vocabulary
420
+ * @param values - the entries
421
+ * @param row - the node index
422
+ */
423
+ function writePropertyValues(
424
+ columns: ColumnWriter,
425
+ vocabulary: Vocabulary,
426
+ values: readonly PropertyValue[],
427
+ row: number,
428
+ ): void {
429
+ const lists = new Map<string, string[]>();
430
+ const others: JsonRecord[] = [];
431
+ for (const { pred, val } of values) {
432
+ const tag = OBOGRAPHS_PREDICATE_TAGS.get(pred);
433
+ if (tag === "shorthand") {
434
+ continue;
435
+ }
436
+ if (tag === undefined) {
437
+ others.push({ relation: vocabulary.id(pred), value: val, datatype: null });
438
+ } else if (OBO_NODE_COLUMNS[tag].dtype === "list") {
439
+ const list = lists.get(tag) ?? [];
440
+ list.push(tag === "alt_id" ? val : vocabulary.id(val));
441
+ lists.set(tag, list);
442
+ } else {
443
+ columns.node(tag, row, val);
444
+ }
445
+ }
446
+ for (const [tag, list] of lists) {
447
+ columns.node(tag, row, list);
448
+ }
449
+ if (others.length > 0) {
450
+ columns.node("property_value", row, others);
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Report a field of the wrong JSON type (the field is left unset, the element kept).
456
+ * @param ctx - the context
457
+ * @param message - what is wrong
458
+ */
459
+ function badValue(ctx: ImportContext, message: string): void {
460
+ ctx.report.error("validation-error", JSON_ISSUE.BAD_VALUE, message);
461
+ }
462
+
463
+ /**
464
+ * Read the edges: the ones between properties into the metadata (typedefs "metadata"), the rest
465
+ * into the sink, making a placeholder node for an endpoint missing from `nodes`.
466
+ * @param ctx - the context
467
+ * @param columns - the column writer
468
+ * @param vocabulary - the graph's vocabulary
469
+ * @param edges - the edge records
470
+ * @returns the property edges kept as metadata
471
+ */
472
+ function readEdges(
473
+ ctx: ImportContext,
474
+ columns: ColumnWriter,
475
+ vocabulary: Vocabulary,
476
+ edges: readonly unknown[],
477
+ ): unknown[] {
478
+ const { report, sink } = ctx;
479
+ const propertyEdges: unknown[] = [];
480
+ const dangling: string[] = [];
481
+ let dropped = 0;
482
+ const endpoint = (raw: string, element: string): NodeId | null => {
483
+ const id = ctx.coerceId(vocabulary.id(raw), element);
484
+ if (id === null || sink.indexOf(id) !== INVALID_INDEX) {
485
+ return id;
486
+ }
487
+ dangling.push(raw);
488
+ if (!ctx.options.addMissingNodes) {
489
+ return null;
490
+ }
491
+ const row = ctx.pushNode(id, element);
492
+ if (row >= 0) {
493
+ columns.node(PLACEHOLDER_COLUMN, row, true);
494
+ }
495
+ return id;
496
+ };
497
+ for (let i = 0; i < edges.length; i++) {
498
+ const element = `edges[${i}]`;
499
+ const record = edges[i];
500
+ if (!isJsonObject(record)) {
501
+ ctx.badElement("edge", element);
502
+ continue;
503
+ }
504
+ const subKey = hasKey(record, "sub") ? "sub" : "subj";
505
+ if (subKey === "subj" && hasKey(record, "subj")) {
506
+ report.warnOnce(
507
+ "coercion",
508
+ JSON_ISSUE.OBOGRAPHS_SUBJ,
509
+ `${element} uses the outdated key subj; read as sub`,
510
+ {
511
+ element,
512
+ },
513
+ );
514
+ }
515
+ const sub = record[subKey];
516
+ const { obj, pred } = record;
517
+ if (typeof sub !== "string" || typeof obj !== "string") {
518
+ ctx.missingEndpoint(element, typeof sub === "string" ? "obj" : "sub");
519
+ continue;
520
+ }
521
+ const between =
522
+ (typeof pred === "string" && PROPERTY_PREDICATES.has(pred)) ||
523
+ vocabulary.isProperty(sub) ||
524
+ vocabulary.isProperty(obj);
525
+ if (between && ctx.json.typedefs === "metadata") {
526
+ propertyEdges.push(record);
527
+ continue;
528
+ }
529
+ try {
530
+ const u = endpoint(sub, `${element}.sub`);
531
+ const v = endpoint(obj, `${element}.obj`);
532
+ if (u === null || v === null) {
533
+ dropped++;
534
+ ctx.countSkipped("edge");
535
+ continue;
536
+ }
537
+ const edge = ctx.pushEdge(u, v, "directed", undefined, element);
538
+ if (typeof pred === "string") {
539
+ columns.edge("relation", edge, vocabulary.relation(pred));
540
+ } else {
541
+ badValue(ctx, `${element} has no pred; its relation is unset`);
542
+ }
543
+ if (isJsonObject(record.meta)) {
544
+ columns.edge("meta", edge, record.meta);
545
+ }
546
+ } catch (err) {
547
+ ctx.skip(err, "edge", element);
548
+ }
549
+ }
550
+ if (dangling.length > 0) {
551
+ const shown = [...new Set(dangling)].slice(0, 5).join(", ");
552
+ const action = ctx.options.addMissingNodes
553
+ ? "each became a placeholder node (graphty.placeholder)"
554
+ : `${dropped} edge(s) to them were dropped (addMissingNodes false)`;
555
+ report.warning(
556
+ "validation-error",
557
+ JSON_ISSUE.DANGLING_REFERENCE,
558
+ `${new Set(dangling).size} edge endpoint(s) missing from nodes: ${shown}; ${action}`,
559
+ { element: dangling[0] },
560
+ );
561
+ }
562
+ return propertyEdges;
563
+ }