@graphty/graph-io 0.3.1 → 0.3.2
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/package.json +1 -1
- package/src/common/escape.ts +34 -5
- package/src/formats/gml/importer.ts +24 -11
- package/src/formats/gml/index.ts +3 -0
- package/src/formats/gml/syntax.ts +9 -1
- package/src/formats/graphml/constants.ts +4 -0
- package/src/formats/graphml/importer.ts +42 -0
- package/src/formats/json/importer.ts +13 -0
- package/src/formats/neo4j/importer.ts +67 -1
- package/src/formats/neo4j/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphty/graph-io",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Importers and exporters (GEXF, GraphML, GML, DOT, Pajek, CSV, JSON, Neo4j) for the @graphty/graph-format snapshot",
|
|
5
5
|
"author": "Adam Powers <apowers@ato.ms>",
|
|
6
6
|
"type": "module",
|
package/src/common/escape.ts
CHANGED
|
@@ -88,16 +88,40 @@ export function quoteGmlString(text: string): string {
|
|
|
88
88
|
return `"${escaped}"`;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
const GML_ENTITY = /&(#[0-9]+|#x[0-9a-fA-F]+|
|
|
92
|
-
|
|
91
|
+
const GML_ENTITY = /&(#[0-9]+|#x[0-9a-fA-F]+|[A-Za-z][A-Za-z0-9]*);/g;
|
|
92
|
+
|
|
93
|
+
/** The XML entities and the ISO-8859-1 HTML entities (U+00A0..U+00FF), which GML uses for characters above 127. */
|
|
94
|
+
const GML_NAMED: ReadonlyMap<string, string> = (() => {
|
|
95
|
+
const table = new Map<string, string>([
|
|
96
|
+
["amp", "&"],
|
|
97
|
+
["quot", '"'],
|
|
98
|
+
["lt", "<"],
|
|
99
|
+
["gt", ">"],
|
|
100
|
+
["apos", "'"],
|
|
101
|
+
]);
|
|
102
|
+
const latin1 = (
|
|
103
|
+
"nbsp iexcl cent pound curren yen brvbar sect uml copy ordf laquo not shy reg macr deg plusmn sup2 " +
|
|
104
|
+
"sup3 acute micro para middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest Agrave Aacute Acirc " +
|
|
105
|
+
"Atilde Auml Aring AElig Ccedil Egrave Eacute Ecirc Euml Igrave Iacute Icirc Iuml ETH Ntilde Ograve " +
|
|
106
|
+
"Oacute Ocirc Otilde Ouml times Oslash Ugrave Uacute Ucirc Uuml Yacute THORN szlig agrave aacute " +
|
|
107
|
+
"acirc atilde auml aring aelig ccedil egrave eacute ecirc euml igrave iacute icirc iuml eth ntilde " +
|
|
108
|
+
"ograve oacute ocirc otilde ouml divide oslash ugrave uacute ucirc uuml yacute thorn yuml"
|
|
109
|
+
).split(" ");
|
|
110
|
+
latin1.forEach((name, i) => {
|
|
111
|
+
table.set(name, String.fromCharCode(0xa0 + i));
|
|
112
|
+
});
|
|
113
|
+
return table;
|
|
114
|
+
})();
|
|
93
115
|
|
|
94
116
|
/**
|
|
95
117
|
* Decode the character entities of a GML string body (the inverse of quoteGmlString on the text
|
|
96
|
-
* between the quotes)
|
|
118
|
+
* between the quotes): numeric references, the XML entities and the ISO-8859-1 HTML entities. An
|
|
119
|
+
* unknown named entity is left as written and handed to `onUnknown`.
|
|
97
120
|
* @param body - the text between the quotes
|
|
121
|
+
* @param onUnknown - called with each unknown named entity (`&name;`), when given
|
|
98
122
|
* @returns the decoded text
|
|
99
123
|
*/
|
|
100
|
-
export function decodeGmlString(body: string): string {
|
|
124
|
+
export function decodeGmlString(body: string, onUnknown?: (entity: string) => void): string {
|
|
101
125
|
return body.replace(GML_ENTITY, (whole, entity: string) => {
|
|
102
126
|
if (entity.startsWith("#x")) {
|
|
103
127
|
return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
|
|
@@ -105,7 +129,12 @@ export function decodeGmlString(body: string): string {
|
|
|
105
129
|
if (entity.startsWith("#")) {
|
|
106
130
|
return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
|
|
107
131
|
}
|
|
108
|
-
|
|
132
|
+
const known = GML_NAMED.get(entity);
|
|
133
|
+
if (known === undefined) {
|
|
134
|
+
onUnknown?.(whole);
|
|
135
|
+
return whole;
|
|
136
|
+
}
|
|
137
|
+
return known;
|
|
109
138
|
});
|
|
110
139
|
}
|
|
111
140
|
|
|
@@ -108,6 +108,8 @@ export const ELEMENT_TYPE_CODE = "E_GML_ELEMENT_TYPE";
|
|
|
108
108
|
export const FLAG_TYPE_CODE = "E_GML_FLAG_TYPE";
|
|
109
109
|
/** Issue code: a `directed` / `multigraph` flag that is not 0 or 1 (read as its truth value), or one repeated. */
|
|
110
110
|
export const FLAG_VALUE_CODE = "W_GML_FLAG_VALUE";
|
|
111
|
+
/** Issue code: a named entity in a string that is neither an XML nor an ISO-8859-1 HTML entity; it is kept as written. */
|
|
112
|
+
export const UNKNOWN_ENTITY_CODE = "W_GML_UNKNOWN_ENTITY";
|
|
111
113
|
/** Issue code: an integer beyond 2^53 stored as the nearest f64 (design section 5.1). */
|
|
112
114
|
export const PRECISION_CODE = SHARED_PRECISION_CODE;
|
|
113
115
|
/** Issue code: the sink already holds a column of the name with another declaration; renamed `<name>#<key>`. */
|
|
@@ -342,6 +344,15 @@ class GmlImport {
|
|
|
342
344
|
*/
|
|
343
345
|
run(tokens: GmlTokens): void {
|
|
344
346
|
this.tokens = tokens;
|
|
347
|
+
tokens.onUnknownEntity = (entity, line) => {
|
|
348
|
+
this.report.warnOnce(
|
|
349
|
+
"parse-error",
|
|
350
|
+
UNKNOWN_ENTITY_CODE,
|
|
351
|
+
`unknown character entity ${entity} is kept as written`,
|
|
352
|
+
{ line, element: entity },
|
|
353
|
+
`${UNKNOWN_ENTITY_CODE}:${entity}`,
|
|
354
|
+
);
|
|
355
|
+
};
|
|
345
356
|
this.scan();
|
|
346
357
|
this.push();
|
|
347
358
|
}
|
|
@@ -438,7 +449,7 @@ class GmlImport {
|
|
|
438
449
|
continue;
|
|
439
450
|
}
|
|
440
451
|
} else {
|
|
441
|
-
if (key === "source" || key === "target") {
|
|
452
|
+
if (key === "source" || key === "target" || key === "directed") {
|
|
442
453
|
continue;
|
|
443
454
|
}
|
|
444
455
|
if (key === weightFrom) {
|
|
@@ -717,7 +728,7 @@ class GmlImport {
|
|
|
717
728
|
this.report.error(
|
|
718
729
|
"validation-error",
|
|
719
730
|
FLAG_TYPE_CODE,
|
|
720
|
-
`
|
|
731
|
+
`flag "${name}" must be the integer 0 or 1, found ${describeValue(t, v)}`,
|
|
721
732
|
{ line: t.line[v], element: name },
|
|
722
733
|
);
|
|
723
734
|
return null;
|
|
@@ -727,7 +738,7 @@ class GmlImport {
|
|
|
727
738
|
this.report.warning(
|
|
728
739
|
"validation-error",
|
|
729
740
|
FLAG_VALUE_CODE,
|
|
730
|
-
`
|
|
741
|
+
`flag "${name}" is ${n}; read as ${n !== 0 ? "1" : "0"}`,
|
|
731
742
|
{ line: t.line[v], element: name },
|
|
732
743
|
);
|
|
733
744
|
}
|
|
@@ -931,12 +942,15 @@ class GmlImport {
|
|
|
931
942
|
let sourceTok = -1;
|
|
932
943
|
let targetTok = -1;
|
|
933
944
|
let weightTok = -1;
|
|
945
|
+
let directedTok = -1;
|
|
934
946
|
for (let p = open + 1; p < close; p = t.nextPair(p)) {
|
|
935
947
|
const key = t.textOf(p);
|
|
936
948
|
if (key === "source") {
|
|
937
949
|
sourceTok = this.structuralToken(sourceTok, p, "source");
|
|
938
950
|
} else if (key === "target") {
|
|
939
951
|
targetTok = this.structuralToken(targetTok, p, "target");
|
|
952
|
+
} else if (key === "directed") {
|
|
953
|
+
directedTok = this.structuralToken(directedTok, p, "directed");
|
|
940
954
|
} else if (key === options.weightFrom) {
|
|
941
955
|
weightTok = p + 1;
|
|
942
956
|
}
|
|
@@ -952,13 +966,12 @@ class GmlImport {
|
|
|
952
966
|
const before = sink.edgeCount;
|
|
953
967
|
const sourceNew = sink.indexOf(source) === INVALID_INDEX;
|
|
954
968
|
const targetNew = source !== target && sink.indexOf(target) === INVALID_INDEX;
|
|
955
|
-
edge
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
);
|
|
969
|
+
// an edge-level `directed` key overrides the graph's flag for that edge (mixed graphs)
|
|
970
|
+
const directed = this.readFlag(directedTok, "directed") ?? this.headerDirected;
|
|
971
|
+
edge = this.requireResolver().addEdge(source, target, directed ? "directed" : "undirected", weight, {
|
|
972
|
+
line,
|
|
973
|
+
element,
|
|
974
|
+
});
|
|
962
975
|
report.counts.edges += sink.edgeCount - before;
|
|
963
976
|
// endpoints the file never declares (addMissingNodes) are nodes of the sink too
|
|
964
977
|
report.counts.nodes += (sourceNew ? 1 : 0) + (targetNew ? 1 : 0);
|
|
@@ -971,7 +984,7 @@ class GmlImport {
|
|
|
971
984
|
try {
|
|
972
985
|
for (let p = open + 1; p < close; p = t.nextPair(p)) {
|
|
973
986
|
const key = t.textOf(p);
|
|
974
|
-
if (key === "source" || key === "target" || key === options.weightFrom) {
|
|
987
|
+
if (key === "source" || key === "target" || key === "directed" || key === options.weightFrom) {
|
|
975
988
|
continue;
|
|
976
989
|
}
|
|
977
990
|
const plan = this.edgePlans.get(key);
|
package/src/formats/gml/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
REPEATED_KEY_CODE,
|
|
46
46
|
ROLE_TAKEN_CODE,
|
|
47
47
|
SECOND_GRAPH_CODE,
|
|
48
|
+
UNKNOWN_ENTITY_CODE,
|
|
48
49
|
} from "./importer.js";
|
|
49
50
|
|
|
50
51
|
export { gmlExporter, type GmlExportOptions } from "./exporter.js";
|
|
@@ -88,6 +89,8 @@ export const GML_ISSUE = Object.freeze({
|
|
|
88
89
|
FLAG_TYPE: FLAG_TYPE_CODE,
|
|
89
90
|
/** A `directed` / `multigraph` flag outside 0 / 1, or repeated. */
|
|
90
91
|
FLAG_VALUE: FLAG_VALUE_CODE,
|
|
92
|
+
/** A named entity in a string that no table decodes; kept as written. */
|
|
93
|
+
UNKNOWN_ENTITY: UNKNOWN_ENTITY_CODE,
|
|
91
94
|
/** An integer beyond 2^53 rounded to f64. */
|
|
92
95
|
PRECISION: PRECISION_CODE,
|
|
93
96
|
/** A column renamed `<name>#<key>` because the sink held the name with another shape. */
|
|
@@ -112,6 +112,9 @@ export class GmlTokens {
|
|
|
112
112
|
/** The number of tokens. */
|
|
113
113
|
count = 0;
|
|
114
114
|
|
|
115
|
+
/** Called by stringOf() with each named entity it cannot decode and the token's line, when set. */
|
|
116
|
+
onUnknownEntity: ((entity: string, line: number) => void) | null = null;
|
|
117
|
+
|
|
115
118
|
/**
|
|
116
119
|
* Create an empty token list over a text.
|
|
117
120
|
* @param text - the source text
|
|
@@ -159,7 +162,12 @@ export class GmlTokens {
|
|
|
159
162
|
* @returns the body with character references decoded
|
|
160
163
|
*/
|
|
161
164
|
stringOf(i: number): string {
|
|
162
|
-
|
|
165
|
+
const text = this.textOf(i);
|
|
166
|
+
if (text.indexOf("&") < 0) {
|
|
167
|
+
return text;
|
|
168
|
+
}
|
|
169
|
+
const report = this.onUnknownEntity;
|
|
170
|
+
return decodeGmlString(text, report === null ? undefined : (entity) => { report(entity, this.line[i]); });
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
/**
|
|
@@ -173,6 +173,10 @@ export const GRAPHML_ISSUE = Object.freeze({
|
|
|
173
173
|
HYPEREDGE_ENDPOINT: "E_GRAPHML_HYPEREDGE_ENDPOINT",
|
|
174
174
|
/** `<port>` declarations (and their data) are not kept; sourceport / targetport edge attributes are. */
|
|
175
175
|
PORT_DECLARATION: "W_GRAPHML_PORT_DECLARATION",
|
|
176
|
+
/** A GraphML `parse.*` hint on `<graph>`, `<node>` or `<edge>` the importer does not act on (parse.nodeids, parse.order, ...). */
|
|
177
|
+
PARSE_HINT_IGNORED: "W_GRAPHML_PARSE_HINT_IGNORED",
|
|
178
|
+
/** An XML attribute GraphML does not define on `<graph>`, `<node>` or `<edge>`; it is not kept. */
|
|
179
|
+
UNKNOWN_XML_ATTRIBUTE: "W_GRAPHML_UNKNOWN_XML_ATTRIBUTE",
|
|
176
180
|
/** A `<locator>` element. */
|
|
177
181
|
LOCATOR_DROPPED: "W_GRAPHML_LOCATOR_DROPPED",
|
|
178
182
|
/** A `<desc>` of a node, an edge or a hyperedge. */
|
|
@@ -83,6 +83,12 @@ import {
|
|
|
83
83
|
} from "./constants.js";
|
|
84
84
|
import { XmlTreeBuilder } from "./tree.js";
|
|
85
85
|
|
|
86
|
+
|
|
87
|
+
/** The XML attributes the importer reads on `<graph>`, `<node>` and `<edge>`; any other is reported. */
|
|
88
|
+
const GRAPH_ATTRIBUTES: ReadonlySet<string> = new Set(["id", "edgedefault", "parse.nodes", "parse.edges"]);
|
|
89
|
+
const NODE_ATTRIBUTES: ReadonlySet<string> = new Set(["id"]);
|
|
90
|
+
const EDGE_ATTRIBUTES: ReadonlySet<string> = new Set(["id", "source", "target", "directed", "sourceport", "targetport"]);
|
|
91
|
+
|
|
86
92
|
/** The format-specific options of the GraphML importer. */
|
|
87
93
|
export interface GraphmlImportOptions {
|
|
88
94
|
/**
|
|
@@ -827,6 +833,39 @@ class GraphmlReader implements XmlHandler {
|
|
|
827
833
|
|
|
828
834
|
// ------------------------------------------------------------------ graphs
|
|
829
835
|
|
|
836
|
+
/**
|
|
837
|
+
* Report, once per element kind and attribute name, an XML attribute the importer does not read:
|
|
838
|
+
* a GraphML `parse.*` hint as ignored, anything else as unknown. Namespace declarations and
|
|
839
|
+
* `xml:` / `xsi:` attributes are not data and pass silently.
|
|
840
|
+
* @param kind - the element name
|
|
841
|
+
* @param attrs - the element's attributes
|
|
842
|
+
* @param known - the attributes the importer reads on that element
|
|
843
|
+
* @param line - the line
|
|
844
|
+
*/
|
|
845
|
+
private reportUnreadAttributes(
|
|
846
|
+
kind: string,
|
|
847
|
+
attrs: ReadonlyMap<string, string>,
|
|
848
|
+
known: ReadonlySet<string>,
|
|
849
|
+
line: number,
|
|
850
|
+
): void {
|
|
851
|
+
for (const name of attrs.keys()) {
|
|
852
|
+
if (known.has(name) || name === "xmlns" || /^(xmlns|xml|xsi):/.test(name)) {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const hint = name.startsWith("parse.");
|
|
856
|
+
const code = hint ? GRAPHML_ISSUE.PARSE_HINT_IGNORED : GRAPHML_ISSUE.UNKNOWN_XML_ATTRIBUTE;
|
|
857
|
+
this.report.warnOnce(
|
|
858
|
+
"unsupported",
|
|
859
|
+
code,
|
|
860
|
+
hint
|
|
861
|
+
? `the <${kind}> parse hint ${name} is ignored`
|
|
862
|
+
: `the <${kind}> attribute ${name} is not a GraphML attribute; it is not kept`,
|
|
863
|
+
{ line, element: name },
|
|
864
|
+
`${code}:${kind}:${name}`,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
830
869
|
/**
|
|
831
870
|
* Open a `<graph>`: the top-level one sets the sink's direction (rule 1 of design section
|
|
832
871
|
* 8.4); a nested one records its container as the parent of its nodes.
|
|
@@ -835,6 +874,7 @@ class GraphmlReader implements XmlHandler {
|
|
|
835
874
|
* @param parent - the containing node's index, or INVALID_INDEX
|
|
836
875
|
*/
|
|
837
876
|
private beginGraph(attrs: ReadonlyMap<string, string>, line: number, parent: number): void {
|
|
877
|
+
this.reportUnreadAttributes("graph", attrs, GRAPH_ATTRIBUTES, line);
|
|
838
878
|
const top = this.graphs.length === 0;
|
|
839
879
|
const edgedefault = attrs.get("edgedefault");
|
|
840
880
|
let directed: boolean;
|
|
@@ -1409,6 +1449,7 @@ class GraphmlReader implements XmlHandler {
|
|
|
1409
1449
|
* @param line - the line
|
|
1410
1450
|
*/
|
|
1411
1451
|
private beginNode(attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1452
|
+
this.reportUnreadAttributes("node", attrs, NODE_ATTRIBUTES, line);
|
|
1412
1453
|
const graph = this.graphs[this.graphs.length - 1];
|
|
1413
1454
|
const state: NodeState = {
|
|
1414
1455
|
id: null,
|
|
@@ -1530,6 +1571,7 @@ class GraphmlReader implements XmlHandler {
|
|
|
1530
1571
|
* @param line - the line
|
|
1531
1572
|
*/
|
|
1532
1573
|
private beginEdge(attrs: ReadonlyMap<string, string>, line: number): void {
|
|
1574
|
+
this.reportUnreadAttributes("edge", attrs, EDGE_ATTRIBUTES, line);
|
|
1533
1575
|
const graph = this.graphs[this.graphs.length - 1];
|
|
1534
1576
|
const edge: EdgeState = {
|
|
1535
1577
|
source: null,
|
|
@@ -157,6 +157,8 @@ export const JSON_ISSUE = Object.freeze({
|
|
|
157
157
|
HYPEREDGE_SHAPE: "E_HYPEREDGE_SHAPE",
|
|
158
158
|
/** The nodes have no id key at all; array positions became the ids. */
|
|
159
159
|
POSITIONAL_NODES: "W_POSITIONAL_NODES",
|
|
160
|
+
/** A node-link / d3 top-level key the importer does not read (the other of edges / links, an unknown key); it is dropped. */
|
|
161
|
+
UNREAD_KEY: "W_JSON_UNREAD_KEY",
|
|
160
162
|
/** A builder-policy option (addMissingNodes, duplicateEdges, selfLoops, weightDtype) differs from the sink's (the shared W_SINK_OPTION). */
|
|
161
163
|
SINK_OPTION: SINK_OPTION_CODE,
|
|
162
164
|
/** A common option the dialect has no use for (nodeIdFrom outside node-link, long, restoreMangledIds). */
|
|
@@ -1120,6 +1122,17 @@ function importNodeLink(ctx: ImportContext, root: JsonRecord, dialect: JsonDiale
|
|
|
1120
1122
|
const multigraph = hasKey(root, "multigraph") ? flagOf(root.multigraph, "multigraph", false, report) : null;
|
|
1121
1123
|
ctx.setHeader(directed);
|
|
1122
1124
|
ctx.writeGraphDict(root.graph, "graph");
|
|
1125
|
+
for (const key of Object.keys(root)) {
|
|
1126
|
+
if (key !== "nodes" && key !== edgesKey && key !== "directed" && key !== "multigraph" && key !== "graph") {
|
|
1127
|
+
const value = root[key];
|
|
1128
|
+
const what = Array.isArray(value)
|
|
1129
|
+
? `${String(value.length)} ${value.length === 1 ? "entry" : "entries"}`
|
|
1130
|
+
: describe(value);
|
|
1131
|
+
report.warning("unsupported", JSON_ISSUE.UNREAD_KEY, `top-level key ${key} (${what}) is not read; dropped`, {
|
|
1132
|
+
element: key,
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1123
1136
|
|
|
1124
1137
|
const nodeList = nodes ?? [];
|
|
1125
1138
|
const edgeList = edges ?? [];
|
|
@@ -122,6 +122,12 @@ export const DUPLICATE_NODE_CODE = SHARED_DUPLICATE_NODE_CODE;
|
|
|
122
122
|
/** Issue code: a node id was declared in two id spaces; the core has one id space and the later row is skipped. */
|
|
123
123
|
export const ID_SPACE_COLLISION_CODE = "E_NEO4J_ID_SPACE_COLLISION";
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Issue code: a `:START_ID(Space)` / `:END_ID(Space)` id names a node a node row declared in another
|
|
127
|
+
* id space; the core has one id space, so the endpoint does not exist in its space and the row is skipped.
|
|
128
|
+
*/
|
|
129
|
+
export const ENDPOINT_SPACE_CODE = "E_NEO4J_ENDPOINT_SPACE";
|
|
130
|
+
|
|
125
131
|
/** Issue code: two different id cells became one id under `ids: "number"`. */
|
|
126
132
|
export const ID_MERGED_CODE = SHARED_ID_MERGED_CODE;
|
|
127
133
|
|
|
@@ -225,6 +231,9 @@ interface RelationshipSection {
|
|
|
225
231
|
readonly width: number;
|
|
226
232
|
readonly startCell: number;
|
|
227
233
|
readonly endCell: number;
|
|
234
|
+
/** The id space of `:START_ID` / `:END_ID` as a registry code, or 0 when the header declares none. */
|
|
235
|
+
readonly startSpaceCode: number;
|
|
236
|
+
readonly endSpaceCode: number;
|
|
228
237
|
/** The `:TYPE` cell, or -1. */
|
|
229
238
|
readonly typeCell: number;
|
|
230
239
|
/** The cell of the property named by `weightFrom`, or -1. */
|
|
@@ -340,6 +349,15 @@ class NodeRegistry {
|
|
|
340
349
|
}
|
|
341
350
|
return previous === code ? "duplicate" : "collision";
|
|
342
351
|
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The space code a node row declared a node in.
|
|
355
|
+
* @param index - the node index
|
|
356
|
+
* @returns the code, or 0 when no node row declared the node
|
|
357
|
+
*/
|
|
358
|
+
codeAt(index: number): number {
|
|
359
|
+
return index < this.codes.length ? this.codes[index] : 0;
|
|
360
|
+
}
|
|
343
361
|
}
|
|
344
362
|
|
|
345
363
|
/**
|
|
@@ -675,7 +693,19 @@ class Neo4jImportSession {
|
|
|
675
693
|
weightCell = fields.findIndex((field) => field.kind === "PROPERTY" && field.name === weightFrom);
|
|
676
694
|
}
|
|
677
695
|
const properties = this.declareProperties("edge", fields, line, weightCell);
|
|
678
|
-
|
|
696
|
+
const { space: startSpace } = fields[startCell];
|
|
697
|
+
const { space: endSpace } = fields[endCell];
|
|
698
|
+
return {
|
|
699
|
+
kind: "relationship",
|
|
700
|
+
width: fields.length,
|
|
701
|
+
startCell,
|
|
702
|
+
endCell,
|
|
703
|
+
startSpaceCode: startSpace === null ? 0 : this.registry.codeOf(startSpace),
|
|
704
|
+
endSpaceCode: endSpace === null ? 0 : this.registry.codeOf(endSpace),
|
|
705
|
+
typeCell,
|
|
706
|
+
weightCell,
|
|
707
|
+
properties,
|
|
708
|
+
};
|
|
679
709
|
}
|
|
680
710
|
|
|
681
711
|
/**
|
|
@@ -944,6 +974,22 @@ class Neo4jImportSession {
|
|
|
944
974
|
report.counts.skippedEdges++;
|
|
945
975
|
return;
|
|
946
976
|
}
|
|
977
|
+
let wrongSpace: string | null = null;
|
|
978
|
+
if (this.wrongSpace(source, section.startSpaceCode)) {
|
|
979
|
+
wrongSpace = startText;
|
|
980
|
+
} else if (this.wrongSpace(target, section.endSpaceCode)) {
|
|
981
|
+
wrongSpace = endText;
|
|
982
|
+
}
|
|
983
|
+
if (wrongSpace !== null) {
|
|
984
|
+
report.error(
|
|
985
|
+
"missing-value",
|
|
986
|
+
ENDPOINT_SPACE_CODE,
|
|
987
|
+
`endpoint ${wrongSpace} is not a node of its declared id space (a node of another id space has that id); the row is skipped`,
|
|
988
|
+
{ line, element },
|
|
989
|
+
);
|
|
990
|
+
report.counts.skippedEdges++;
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
947
993
|
let weight: number | undefined;
|
|
948
994
|
if (section.weightCell >= 0) {
|
|
949
995
|
try {
|
|
@@ -977,6 +1023,26 @@ class Neo4jImportSession {
|
|
|
977
1023
|
report.counts.edges++;
|
|
978
1024
|
}
|
|
979
1025
|
|
|
1026
|
+
/**
|
|
1027
|
+
* Whether an endpoint id belongs to a node a node row declared in another id space than the
|
|
1028
|
+
* endpoint's header declares. An endpoint without a declared space, or a node no row declared
|
|
1029
|
+
* yet, is looked up by id alone.
|
|
1030
|
+
* @param id - the endpoint id
|
|
1031
|
+
* @param spaceCode - the endpoint's space code, 0 for none
|
|
1032
|
+
* @returns true when the endpoint resolves to a node of another space
|
|
1033
|
+
*/
|
|
1034
|
+
private wrongSpace(id: NodeId, spaceCode: number): boolean {
|
|
1035
|
+
if (spaceCode === 0) {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
const index = this.sink.indexOf(id);
|
|
1039
|
+
if (index < 0) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
const declared = this.registry.codeAt(index);
|
|
1043
|
+
return declared !== 0 && declared !== spaceCode;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
980
1046
|
/**
|
|
981
1047
|
* Coerce an id cell, reporting a merge under `ids: "number"` and an invalid id as an error.
|
|
982
1048
|
* @param text - the cell
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import {
|
|
33
33
|
COLUMN_COUNT_CODE,
|
|
34
34
|
DUPLICATE_NODE_CODE,
|
|
35
|
+
ENDPOINT_SPACE_CODE,
|
|
35
36
|
HEADER_CODE,
|
|
36
37
|
HEADER_OPTION_CODE,
|
|
37
38
|
ID_MERGED_CODE,
|
|
@@ -75,6 +76,8 @@ export const NEO4J_ISSUE = Object.freeze({
|
|
|
75
76
|
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
76
77
|
/** A node id declared in two id spaces. */
|
|
77
78
|
ID_SPACE_COLLISION: ID_SPACE_COLLISION_CODE,
|
|
79
|
+
/** A relationship endpoint whose id belongs to a node of another id space; the row is skipped. */
|
|
80
|
+
ENDPOINT_SPACE: ENDPOINT_SPACE_CODE,
|
|
78
81
|
/** Two id texts merged into one number under ids "number". */
|
|
79
82
|
ID_MERGED: ID_MERGED_CODE,
|
|
80
83
|
/** A header brace option the importer does not apply. */
|