@graphty/graph-io 0.3.3 → 0.3.4

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 (42) hide show
  1. package/README.md +17 -4
  2. package/dist/chunks/{importer-BELrICWM.js → importer-BOxwmef_.js} +352 -19
  3. package/dist/chunks/importer-BOxwmef_.js.map +1 -0
  4. package/dist/graph-io.js +2 -2
  5. package/dist/graph-io.js.map +1 -1
  6. package/dist/json.js +1 -1
  7. package/dist/src/formats/json/dialect.d.ts +21 -4
  8. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  9. package/dist/src/formats/json/dialect.js +29 -4
  10. package/dist/src/formats/json/dialect.js.map +1 -1
  11. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  12. package/dist/src/formats/json/exporter.js +27 -7
  13. package/dist/src/formats/json/exporter.js.map +1 -1
  14. package/dist/src/formats/json/importer.d.ts +17 -4
  15. package/dist/src/formats/json/importer.d.ts.map +1 -1
  16. package/dist/src/formats/json/importer.js +356 -8
  17. package/dist/src/formats/json/importer.js.map +1 -1
  18. package/dist/src/formats/json/index.d.ts +1 -1
  19. package/dist/src/formats/json/index.d.ts.map +1 -1
  20. package/dist/src/formats/json/index.js +1 -1
  21. package/dist/src/formats/json/index.js.map +1 -1
  22. package/dist/src/index.d.ts +1 -1
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/sniff.d.ts +3 -3
  26. package/dist/src/sniff.d.ts.map +1 -1
  27. package/dist/src/sniff.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/common/escape.ts +5 -4
  30. package/src/formats/json/dialect.ts +40 -6
  31. package/src/formats/json/exporter.ts +28 -7
  32. package/src/formats/json/importer.ts +411 -21
  33. package/src/formats/json/index.ts +7 -1
  34. package/src/formats/neo4j/exporter.ts +43 -6
  35. package/src/formats/neo4j/importer.ts +56 -11
  36. package/src/formats/neo4j/index.ts +10 -3
  37. package/src/formats/pajek/exporter.ts +3 -1
  38. package/src/formats/pajek/importer.ts +284 -60
  39. package/src/formats/pajek/syntax.ts +98 -12
  40. package/src/index.ts +2 -0
  41. package/src/sniff.ts +3 -3
  42. package/dist/chunks/importer-BELrICWM.js.map +0 -1
@@ -19,6 +19,14 @@
19
19
  * `idSpace`), and a stored id `name:ID(Space)` also a string node column `name` whose
20
20
  * `origin.namespace` is the space. A property whose name collides with one of those is renamed
21
21
  * `<name>#<name>` (design section 5.6).
22
+ * - Id spaces: neo4j-admin keeps one id space per `:ID(Space)` name, so `1` in `:ID(Product)` and
23
+ * `1` in `:ID(Category)` are two nodes. The core has one id space, so a node of a spaced section
24
+ * is stored under the string id `Space:id` (every row of the section, collision or not, so an id
25
+ * never depends on file order), its id text goes into the node string column `originalId` and
26
+ * its space into `idSpace`. The column has no role: the `originalId` role means an id rewritten by
27
+ * `sanitizeIds: "mangle"`, which other importers restore as the node id. `:START_ID(Space)` /
28
+ * `:END_ID(Space)` endpoints are qualified the same way before the lookup; an endpoint without a
29
+ * space is looked up as written.
22
30
  * - Ids are text cells coerced by the `ids` option ("canonical" by default, so `1` is the number 1
23
31
  * and `007` stays a string; integers beyond 2^53 stay strings). Relationships are always directed
24
32
  * ("In Neo4j, all relationships have a direction"), so the sink is set directed before the first
@@ -104,6 +112,9 @@ export const TYPE_COLUMN = "type";
104
112
  /** The name of the node dict column holding the id space of `:ID(Space)`. */
105
113
  export const ID_SPACE_COLUMN = "idSpace";
106
114
 
115
+ /** The name of the node string column holding the id text of a node of an id space (its id is `Space:id`). */
116
+ export const ORIGINAL_ID_COLUMN = "originalId";
117
+
107
118
  /** Issue code: a header row (or a whole section) is malformed; the import aborts. */
108
119
  export const HEADER_CODE = "E_NEO4J_HEADER";
109
120
 
@@ -119,12 +130,15 @@ export const MISSING_ENDPOINT_CODE = SHARED_MISSING_ENDPOINT_CODE;
119
130
  /** Issue code: a node id was declared twice (same id space); the later row's properties win. */
120
131
  export const DUPLICATE_NODE_CODE = SHARED_DUPLICATE_NODE_CODE;
121
132
 
122
- /** Issue code: a node id was declared in two id spaces; the core has one id space and the later row is skipped. */
133
+ /**
134
+ * Issue code: a node id was declared in two id spaces -- a spaced id `Space:id` equals the text of an
135
+ * id declared without a space; the later row is skipped.
136
+ */
123
137
  export const ID_SPACE_COLLISION_CODE = "E_NEO4J_ID_SPACE_COLLISION";
124
138
 
125
139
  /**
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.
140
+ * Issue code: a `:START_ID(Space)` / `:END_ID(Space)` endpoint's qualified id `Space:id` names a node a
141
+ * node row declared in another id space (without a space); the row is skipped.
128
142
  */
129
143
  export const ENDPOINT_SPACE_CODE = "E_NEO4J_ENDPOINT_SPACE";
130
144
 
@@ -185,6 +199,14 @@ const ID_SPACE_DECL: ColumnDecl = {
185
199
  origin: { format: NEO4J, id: ":ID", title: null, type: "ID", namespace: null },
186
200
  };
187
201
 
202
+ const ORIGINAL_ID_DECL: ColumnDecl = {
203
+ name: ORIGINAL_ID_COLUMN,
204
+ dtype: "string",
205
+ nullable: true,
206
+ // origin.type stays null so the column is never mistaken for a stored id (`name:ID`)
207
+ origin: { format: NEO4J, id: ":ID", title: null, type: null, namespace: null },
208
+ };
209
+
188
210
  const ARRAY_DELIMITERS: Readonly<Record<string, ListSyntax>> = { ";": "semicolon", ",": "comma", "|": "pipe" };
189
211
 
190
212
  /** The resolved format-specific options. */
@@ -231,6 +253,9 @@ interface RelationshipSection {
231
253
  readonly width: number;
232
254
  readonly startCell: number;
233
255
  readonly endCell: number;
256
+ /** The id space of `:START_ID` / `:END_ID`, or null when the header declares none. */
257
+ readonly startSpace: string | null;
258
+ readonly endSpace: string | null;
234
259
  /** The id space of `:START_ID` / `:END_ID` as a registry code, or 0 when the header declares none. */
235
260
  readonly startSpaceCode: number;
236
261
  readonly endSpaceCode: number;
@@ -443,6 +468,8 @@ class Neo4jImportSession {
443
468
 
444
469
  private idSpaceHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
445
470
 
471
+ private originalIdHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
472
+
446
473
  private ignoredColumns = 0;
447
474
 
448
475
  /** Whether the sink's direction was set (before the first relationship, design section 8.4 rule 1). */
@@ -700,6 +727,8 @@ class Neo4jImportSession {
700
727
  width: fields.length,
701
728
  startCell,
702
729
  endCell,
730
+ startSpace,
731
+ endSpace,
703
732
  startSpaceCode: startSpace === null ? 0 : this.registry.codeOf(startSpace),
704
733
  endSpaceCode: endSpace === null ? 0 : this.registry.codeOf(endSpace),
705
734
  typeCell,
@@ -809,6 +838,7 @@ class Neo4jImportSession {
809
838
  private ensureIdSpace(): void {
810
839
  if (this.idSpaceHandle === INVALID_INDEX) {
811
840
  this.idSpaceHandle = this.declareReserved("node", ID_SPACE_DECL);
841
+ this.originalIdHandle = this.declareReserved("node", ORIGINAL_ID_DECL);
812
842
  }
813
843
  }
814
844
 
@@ -881,11 +911,12 @@ class Neo4jImportSession {
881
911
  report.counts.skippedNodes++;
882
912
  return;
883
913
  }
884
- const id = this.coerceId(idText, line);
885
- if (id === null || !this.parseProperties(section.properties, cells, quoted, line, idText)) {
914
+ const coerced = this.coerceId(idText, line);
915
+ if (coerced === null || !this.parseProperties(section.properties, cells, quoted, line, idText)) {
886
916
  report.counts.skippedNodes++;
887
917
  return;
888
918
  }
919
+ const id = qualify(coerced, section.space);
889
920
  let labels: string[] | undefined;
890
921
  if (section.labelCells.length > 0 || section.extraLabels.length > 0) {
891
922
  labels = this.labelsOf(section, cells, quoted);
@@ -918,6 +949,7 @@ class Neo4jImportSession {
918
949
  }
919
950
  if (section.space !== null) {
920
951
  sink.setNodeValue(this.idSpaceHandle, index, section.space);
952
+ sink.setNodeValue(this.originalIdHandle, index, idText);
921
953
  }
922
954
  if (labels !== undefined) {
923
955
  sink.setNodeValue(this.labelsHandle, index, labels);
@@ -968,12 +1000,14 @@ class Neo4jImportSession {
968
1000
  return;
969
1001
  }
970
1002
  const element = `${startText}->${endText}`;
971
- const source = this.coerceId(startText, line);
972
- const target = source === null ? null : this.coerceId(endText, line);
973
- if (source === null || target === null) {
1003
+ const start = this.coerceId(startText, line);
1004
+ const end = start === null ? null : this.coerceId(endText, line);
1005
+ if (start === null || end === null) {
974
1006
  report.counts.skippedEdges++;
975
1007
  return;
976
1008
  }
1009
+ const source = qualify(start, section.startSpace);
1010
+ const target = qualify(end, section.endSpace);
977
1011
  let wrongSpace: string | null = null;
978
1012
  if (this.wrongSpace(source, section.startSpaceCode)) {
979
1013
  wrongSpace = startText;
@@ -1024,9 +1058,9 @@ class Neo4jImportSession {
1024
1058
  }
1025
1059
 
1026
1060
  /**
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.
1061
+ * Whether a qualified endpoint id belongs to a node a node row declared in another id space than
1062
+ * the endpoint's header declares (an unspaced node whose id text is `Space:id`). An endpoint
1063
+ * without a declared space, or a node no row declared yet, is looked up by id alone.
1030
1064
  * @param id - the endpoint id
1031
1065
  * @param spaceCode - the endpoint's space code, 0 for none
1032
1066
  * @returns true when the endpoint resolves to a node of another space
@@ -1197,6 +1231,17 @@ class Neo4jImportSession {
1197
1231
  }
1198
1232
  }
1199
1233
 
1234
+ /**
1235
+ * The id a node of an id space is stored under: `Space:id`, so the same id in two spaces stays two
1236
+ * nodes (the core has one id space).
1237
+ * @param id - the coerced id cell
1238
+ * @param space - the id space, or null
1239
+ * @returns the id unchanged without a space, else the string `Space:id`
1240
+ */
1241
+ function qualify(id: NodeId, space: string | null): NodeId {
1242
+ return space === null ? id : `${space}:${String(id)}`;
1243
+ }
1244
+
1200
1245
  /**
1201
1246
  * The value of a quoted empty cell: an empty string for a string-kind property, an empty list for
1202
1247
  * a list property, unset for anything else (neo4j-admin's default `--ignore-empty-strings=false`).
@@ -44,7 +44,14 @@ import {
44
44
  } from "./importer.js";
45
45
 
46
46
  export { NEO4J_CAPABILITIES, neo4jExporter, type Neo4jExportOptions } from "./exporter.js";
47
- export { ID_SPACE_COLUMN, LABELS_COLUMN, neo4jImporter, type Neo4jImportOptions, TYPE_COLUMN } from "./importer.js";
47
+ export {
48
+ ID_SPACE_COLUMN,
49
+ LABELS_COLUMN,
50
+ neo4jImporter,
51
+ type Neo4jImportOptions,
52
+ ORIGINAL_ID_COLUMN,
53
+ TYPE_COLUMN,
54
+ } from "./importer.js";
48
55
 
49
56
  /**
50
57
  * The issue codes the Neo4j importer records (design section 8.6), by name: the codes shared with
@@ -74,9 +81,9 @@ export const NEO4J_ISSUE = Object.freeze({
74
81
  MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
75
82
  /** A node id repeated in one id space (last write wins). */
76
83
  DUPLICATE_NODE: DUPLICATE_NODE_CODE,
77
- /** A node id declared in two id spaces. */
84
+ /** A spaced node id `Space:id` that equals the text of an id declared without a space. */
78
85
  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. */
86
+ /** A spaced relationship endpoint `Space:id` that names a node declared without a space; the row is skipped. */
80
87
  ENDPOINT_SPACE: ENDPOINT_SPACE_CODE,
81
88
  /** Two id texts merged into one number under ids "number". */
82
89
  ID_MERGED: ID_MERGED_CODE,
@@ -25,6 +25,7 @@ import { explicitWeights } from "../../common/weights.js";
25
25
  import { encodeChunks, joinText } from "../../common/writer.js";
26
26
  import { type CommonExportOptions, type ExportCapabilities, type GraphExporter, type LossNote } from "../../types.js";
27
27
  import {
28
+ encodeCharacterReferences,
28
29
  formatIntervals,
29
30
  isParameterKey,
30
31
  LABEL_COLUMN,
@@ -771,7 +772,8 @@ function* writeVertices(
771
772
  const label = labelOf(labels, ids, i, parts.length > 0);
772
773
  let line = String(i + 1);
773
774
  if (label !== null) {
774
- line += ` ${quotePajekLabel(label)}`;
775
+ // the importer decodes `&#dddd;` in labels, so a literal one is written with `&#38;`
776
+ line += ` ${quotePajekLabel(encodeCharacterReferences(label))}`;
775
777
  }
776
778
  if (parts.length > 0) {
777
779
  line += ` ${parts.join(" ")}`;