@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.
- package/LICENSE +1 -1
- package/README.md +29 -3
- package/dist/chunks/{escape-scjHxjpr.js → escape-D-gZWO26.js} +3 -2
- package/dist/chunks/{escape-scjHxjpr.js.map → escape-D-gZWO26.js.map} +1 -1
- package/dist/chunks/{importer-Xg07gk7p.js → importer-Br_QeAeE.js} +4 -3
- package/dist/chunks/{importer-Xg07gk7p.js.map → importer-Br_QeAeE.js.map} +1 -1
- package/dist/chunks/{importer-ruMWWvVs.js → importer-DHagxvDD.js} +4 -3
- package/dist/chunks/{importer-ruMWWvVs.js.map → importer-DHagxvDD.js.map} +1 -1
- package/dist/chunks/{importer-B6rKRxzv.js → importer-Du5crN9l.js} +509 -25
- package/dist/chunks/importer-Du5crN9l.js.map +1 -0
- package/dist/chunks/importer-aNJfe0qu.js +1614 -0
- package/dist/chunks/importer-aNJfe0qu.js.map +1 -0
- package/dist/chunks/{importer-BmOl9gLW.js → importer-d0uQxFp6.js} +4 -3
- package/dist/chunks/{importer-BmOl9gLW.js.map → importer-d0uQxFp6.js.map} +1 -1
- package/dist/chunks/ontology-BnrJ4I98.js +113 -0
- package/dist/chunks/ontology-BnrJ4I98.js.map +1 -0
- package/dist/chunks/{records-DSpbTE5s.js → records-Bk9jgodz.js} +2 -2
- package/dist/chunks/{records-DSpbTE5s.js.map → records-Bk9jgodz.js.map} +1 -1
- package/dist/chunks/{writer-DHHfHn11.js → report-BOk0p5y8.js} +97 -919
- package/dist/chunks/report-BOk0p5y8.js.map +1 -0
- package/dist/chunks/writer-GAdltGmC.js +827 -0
- package/dist/chunks/writer-GAdltGmC.js.map +1 -0
- package/dist/csv.js +4 -3
- package/dist/csv.js.map +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +3 -2
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +3 -2
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +160 -141
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +1 -1
- package/dist/json.js +1 -1
- package/dist/neo4j.js +4 -3
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.d.ts +1 -0
- package/dist/obo.js +6 -0
- package/dist/obo.js.map +1 -0
- package/dist/pajek.js +1 -1
- package/dist/src/common/ontology.d.ts +59 -0
- package/dist/src/common/ontology.d.ts.map +1 -0
- package/dist/src/common/ontology.js +147 -0
- package/dist/src/common/ontology.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +8 -6
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +26 -3
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +324 -7
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +153 -22
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/obographs.d.ts +21 -0
- package/dist/src/formats/json/obographs.d.ts.map +1 -0
- package/dist/src/formats/json/obographs.js +476 -0
- package/dist/src/formats/json/obographs.js.map +1 -0
- package/dist/src/formats/obo/importer.d.ts +90 -0
- package/dist/src/formats/obo/importer.d.ts.map +1 -0
- package/dist/src/formats/obo/importer.js +1248 -0
- package/dist/src/formats/obo/importer.js.map +1 -0
- package/dist/src/formats/obo/index.d.ts +7 -0
- package/dist/src/formats/obo/index.d.ts.map +1 -0
- package/dist/src/formats/obo/index.js +7 -0
- package/dist/src/formats/obo/index.js.map +1 -0
- package/dist/src/formats/obo/syntax.d.ts +121 -0
- package/dist/src/formats/obo/syntax.d.ts.map +1 -0
- package/dist/src/formats/obo/syntax.js +424 -0
- package/dist/src/formats/obo/syntax.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +3 -1
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts +1 -1
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js +23 -3
- package/dist/src/sniff.js.map +1 -1
- package/package.json +6 -1
- package/src/common/ontology.ts +169 -0
- package/src/formats/json/dialect.ts +37 -7
- package/src/formats/json/importer.ts +204 -27
- package/src/formats/json/obographs.ts +563 -0
- package/src/formats/obo/importer.ts +1695 -0
- package/src/formats/obo/index.ts +7 -0
- package/src/formats/obo/syntax.ts +466 -0
- package/src/index.ts +1 -0
- package/src/registry.ts +3 -1
- package/src/sniff.ts +35 -5
- package/dist/chunks/importer-B6rKRxzv.js.map +0 -1
- 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
|
+
}
|