@graphty/graph-io 0.0.0 → 0.2.1

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 (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -28
  3. package/dist/chunks/children-CL3Cy0ez.js +238 -0
  4. package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
  5. package/dist/chunks/escape-DyI8JofU.js +938 -0
  6. package/dist/chunks/escape-DyI8JofU.js.map +1 -0
  7. package/dist/chunks/importer-CQnJuWJw.js +2987 -0
  8. package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
  9. package/dist/chunks/importer-CpCpfbxr.js +2015 -0
  10. package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
  11. package/dist/chunks/importer-DbnGYr3_.js +2342 -0
  12. package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
  13. package/dist/chunks/importer-GozH8DkN.js +3050 -0
  14. package/dist/chunks/importer-GozH8DkN.js.map +1 -0
  15. package/dist/chunks/records-CGpxszm1.js +605 -0
  16. package/dist/chunks/records-CGpxszm1.js.map +1 -0
  17. package/dist/chunks/text-CajMdVFy.js +189 -0
  18. package/dist/chunks/text-CajMdVFy.js.map +1 -0
  19. package/dist/chunks/writer-DxSKC7TL.js +2842 -0
  20. package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
  21. package/dist/csv.d.ts +1 -0
  22. package/dist/csv.js +1702 -0
  23. package/dist/csv.js.map +1 -0
  24. package/dist/dot.d.ts +1 -0
  25. package/dist/dot.js +8 -0
  26. package/dist/dot.js.map +1 -0
  27. package/dist/gexf.d.ts +1 -0
  28. package/dist/gexf.js +3466 -0
  29. package/dist/gexf.js.map +1 -0
  30. package/dist/gml.d.ts +1 -0
  31. package/dist/gml.js +2647 -0
  32. package/dist/gml.js.map +1 -0
  33. package/dist/graph-io.d.ts +1 -0
  34. package/dist/graph-io.js +790 -0
  35. package/dist/graph-io.js.map +1 -0
  36. package/dist/graphml.d.ts +1 -0
  37. package/dist/graphml.js +8 -0
  38. package/dist/graphml.js.map +1 -0
  39. package/dist/json.d.ts +1 -0
  40. package/dist/json.js +11 -0
  41. package/dist/json.js.map +1 -0
  42. package/dist/neo4j.d.ts +1 -0
  43. package/dist/neo4j.js +2046 -0
  44. package/dist/neo4j.js.map +1 -0
  45. package/dist/pajek.d.ts +1 -0
  46. package/dist/pajek.js +8 -0
  47. package/dist/pajek.js.map +1 -0
  48. package/dist/src/children.d.ts +134 -0
  49. package/dist/src/children.d.ts.map +1 -0
  50. package/dist/src/children.js +274 -0
  51. package/dist/src/children.js.map +1 -0
  52. package/dist/src/common/attributes.d.ts +229 -0
  53. package/dist/src/common/attributes.d.ts.map +1 -0
  54. package/dist/src/common/attributes.js +368 -0
  55. package/dist/src/common/attributes.js.map +1 -0
  56. package/dist/src/common/codes.d.ts +105 -0
  57. package/dist/src/common/codes.d.ts.map +1 -0
  58. package/dist/src/common/codes.js +107 -0
  59. package/dist/src/common/codes.js.map +1 -0
  60. package/dist/src/common/declared-types.d.ts +84 -0
  61. package/dist/src/common/declared-types.d.ts.map +1 -0
  62. package/dist/src/common/declared-types.js +326 -0
  63. package/dist/src/common/declared-types.js.map +1 -0
  64. package/dist/src/common/direction.d.ts +206 -0
  65. package/dist/src/common/direction.d.ts.map +1 -0
  66. package/dist/src/common/direction.js +370 -0
  67. package/dist/src/common/direction.js.map +1 -0
  68. package/dist/src/common/escape.d.ts +92 -0
  69. package/dist/src/common/escape.d.ts.map +1 -0
  70. package/dist/src/common/escape.js +212 -0
  71. package/dist/src/common/escape.js.map +1 -0
  72. package/dist/src/common/export.d.ts +249 -0
  73. package/dist/src/common/export.d.ts.map +1 -0
  74. package/dist/src/common/export.js +594 -0
  75. package/dist/src/common/export.js.map +1 -0
  76. package/dist/src/common/format.d.ts +59 -0
  77. package/dist/src/common/format.d.ts.map +1 -0
  78. package/dist/src/common/format.js +106 -0
  79. package/dist/src/common/format.js.map +1 -0
  80. package/dist/src/common/ids.d.ts +83 -0
  81. package/dist/src/common/ids.d.ts.map +1 -0
  82. package/dist/src/common/ids.js +158 -0
  83. package/dist/src/common/ids.js.map +1 -0
  84. package/dist/src/common/input.d.ts +100 -0
  85. package/dist/src/common/input.d.ts.map +1 -0
  86. package/dist/src/common/input.js +335 -0
  87. package/dist/src/common/input.js.map +1 -0
  88. package/dist/src/common/lists.d.ts +34 -0
  89. package/dist/src/common/lists.d.ts.map +1 -0
  90. package/dist/src/common/lists.js +185 -0
  91. package/dist/src/common/lists.js.map +1 -0
  92. package/dist/src/common/options.d.ts +108 -0
  93. package/dist/src/common/options.d.ts.map +1 -0
  94. package/dist/src/common/options.js +265 -0
  95. package/dist/src/common/options.js.map +1 -0
  96. package/dist/src/common/report.d.ts +187 -0
  97. package/dist/src/common/report.d.ts.map +1 -0
  98. package/dist/src/common/report.js +274 -0
  99. package/dist/src/common/report.js.map +1 -0
  100. package/dist/src/common/temporal.d.ts +71 -0
  101. package/dist/src/common/temporal.d.ts.map +1 -0
  102. package/dist/src/common/temporal.js +266 -0
  103. package/dist/src/common/temporal.js.map +1 -0
  104. package/dist/src/common/text.d.ts +104 -0
  105. package/dist/src/common/text.d.ts.map +1 -0
  106. package/dist/src/common/text.js +255 -0
  107. package/dist/src/common/text.js.map +1 -0
  108. package/dist/src/common/weights.d.ts +77 -0
  109. package/dist/src/common/weights.d.ts.map +1 -0
  110. package/dist/src/common/weights.js +156 -0
  111. package/dist/src/common/weights.js.map +1 -0
  112. package/dist/src/common/writer.d.ts +51 -0
  113. package/dist/src/common/writer.d.ts.map +1 -0
  114. package/dist/src/common/writer.js +108 -0
  115. package/dist/src/common/writer.js.map +1 -0
  116. package/dist/src/common/xml.d.ts +245 -0
  117. package/dist/src/common/xml.d.ts.map +1 -0
  118. package/dist/src/common/xml.js +942 -0
  119. package/dist/src/common/xml.js.map +1 -0
  120. package/dist/src/formats/csv/exporter.d.ts +70 -0
  121. package/dist/src/formats/csv/exporter.d.ts.map +1 -0
  122. package/dist/src/formats/csv/exporter.js +682 -0
  123. package/dist/src/formats/csv/exporter.js.map +1 -0
  124. package/dist/src/formats/csv/header.d.ts +66 -0
  125. package/dist/src/formats/csv/header.d.ts.map +1 -0
  126. package/dist/src/formats/csv/header.js +152 -0
  127. package/dist/src/formats/csv/header.js.map +1 -0
  128. package/dist/src/formats/csv/importer.d.ts +82 -0
  129. package/dist/src/formats/csv/importer.d.ts.map +1 -0
  130. package/dist/src/formats/csv/importer.js +849 -0
  131. package/dist/src/formats/csv/importer.js.map +1 -0
  132. package/dist/src/formats/csv/index.d.ts +60 -0
  133. package/dist/src/formats/csv/index.d.ts.map +1 -0
  134. package/dist/src/formats/csv/index.js +63 -0
  135. package/dist/src/formats/csv/index.js.map +1 -0
  136. package/dist/src/formats/csv/records.d.ts +188 -0
  137. package/dist/src/formats/csv/records.d.ts.map +1 -0
  138. package/dist/src/formats/csv/records.js +702 -0
  139. package/dist/src/formats/csv/records.js.map +1 -0
  140. package/dist/src/formats/csv/values.d.ts +105 -0
  141. package/dist/src/formats/csv/values.d.ts.map +1 -0
  142. package/dist/src/formats/csv/values.js +192 -0
  143. package/dist/src/formats/csv/values.js.map +1 -0
  144. package/dist/src/formats/dot/exporter.d.ts +52 -0
  145. package/dist/src/formats/dot/exporter.d.ts.map +1 -0
  146. package/dist/src/formats/dot/exporter.js +836 -0
  147. package/dist/src/formats/dot/exporter.js.map +1 -0
  148. package/dist/src/formats/dot/importer.d.ts +102 -0
  149. package/dist/src/formats/dot/importer.d.ts.map +1 -0
  150. package/dist/src/formats/dot/importer.js +1291 -0
  151. package/dist/src/formats/dot/importer.js.map +1 -0
  152. package/dist/src/formats/dot/index.d.ts +7 -0
  153. package/dist/src/formats/dot/index.d.ts.map +1 -0
  154. package/dist/src/formats/dot/index.js +7 -0
  155. package/dist/src/formats/dot/index.js.map +1 -0
  156. package/dist/src/formats/dot/names.d.ts +29 -0
  157. package/dist/src/formats/dot/names.d.ts.map +1 -0
  158. package/dist/src/formats/dot/names.js +28 -0
  159. package/dist/src/formats/dot/names.js.map +1 -0
  160. package/dist/src/formats/dot/tokenizer.d.ts +114 -0
  161. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
  162. package/dist/src/formats/dot/tokenizer.js +341 -0
  163. package/dist/src/formats/dot/tokenizer.js.map +1 -0
  164. package/dist/src/formats/gexf/exporter.d.ts +56 -0
  165. package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
  166. package/dist/src/formats/gexf/exporter.js +1395 -0
  167. package/dist/src/formats/gexf/exporter.js.map +1 -0
  168. package/dist/src/formats/gexf/importer.d.ts +73 -0
  169. package/dist/src/formats/gexf/importer.d.ts.map +1 -0
  170. package/dist/src/formats/gexf/importer.js +1880 -0
  171. package/dist/src/formats/gexf/importer.js.map +1 -0
  172. package/dist/src/formats/gexf/index.d.ts +96 -0
  173. package/dist/src/formats/gexf/index.d.ts.map +1 -0
  174. package/dist/src/formats/gexf/index.js +97 -0
  175. package/dist/src/formats/gexf/index.js.map +1 -0
  176. package/dist/src/formats/gexf/schema.d.ts +135 -0
  177. package/dist/src/formats/gexf/schema.d.ts.map +1 -0
  178. package/dist/src/formats/gexf/schema.js +323 -0
  179. package/dist/src/formats/gexf/schema.js.map +1 -0
  180. package/dist/src/formats/gml/exporter.d.ts +69 -0
  181. package/dist/src/formats/gml/exporter.d.ts.map +1 -0
  182. package/dist/src/formats/gml/exporter.js +1093 -0
  183. package/dist/src/formats/gml/exporter.js.map +1 -0
  184. package/dist/src/formats/gml/importer.d.ts +66 -0
  185. package/dist/src/formats/gml/importer.d.ts.map +1 -0
  186. package/dist/src/formats/gml/importer.js +1331 -0
  187. package/dist/src/formats/gml/importer.js.map +1 -0
  188. package/dist/src/formats/gml/index.d.ts +85 -0
  189. package/dist/src/formats/gml/index.d.ts.map +1 -0
  190. package/dist/src/formats/gml/index.js +88 -0
  191. package/dist/src/formats/gml/index.js.map +1 -0
  192. package/dist/src/formats/gml/syntax.d.ts +186 -0
  193. package/dist/src/formats/gml/syntax.d.ts.map +1 -0
  194. package/dist/src/formats/gml/syntax.js +467 -0
  195. package/dist/src/formats/gml/syntax.js.map +1 -0
  196. package/dist/src/formats/graphml/constants.d.ts +169 -0
  197. package/dist/src/formats/graphml/constants.d.ts.map +1 -0
  198. package/dist/src/formats/graphml/constants.js +165 -0
  199. package/dist/src/formats/graphml/constants.js.map +1 -0
  200. package/dist/src/formats/graphml/exporter.d.ts +34 -0
  201. package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
  202. package/dist/src/formats/graphml/exporter.js +1176 -0
  203. package/dist/src/formats/graphml/exporter.js.map +1 -0
  204. package/dist/src/formats/graphml/importer.d.ts +31 -0
  205. package/dist/src/formats/graphml/importer.d.ts.map +1 -0
  206. package/dist/src/formats/graphml/importer.js +1607 -0
  207. package/dist/src/formats/graphml/importer.js.map +1 -0
  208. package/dist/src/formats/graphml/index.d.ts +8 -0
  209. package/dist/src/formats/graphml/index.d.ts.map +1 -0
  210. package/dist/src/formats/graphml/index.js +8 -0
  211. package/dist/src/formats/graphml/index.js.map +1 -0
  212. package/dist/src/formats/graphml/tree.d.ts +72 -0
  213. package/dist/src/formats/graphml/tree.d.ts.map +1 -0
  214. package/dist/src/formats/graphml/tree.js +290 -0
  215. package/dist/src/formats/graphml/tree.js.map +1 -0
  216. package/dist/src/formats/json/dialect.d.ts +125 -0
  217. package/dist/src/formats/json/dialect.d.ts.map +1 -0
  218. package/dist/src/formats/json/dialect.js +262 -0
  219. package/dist/src/formats/json/dialect.js.map +1 -0
  220. package/dist/src/formats/json/exporter.d.ts +89 -0
  221. package/dist/src/formats/json/exporter.d.ts.map +1 -0
  222. package/dist/src/formats/json/exporter.js +1358 -0
  223. package/dist/src/formats/json/exporter.js.map +1 -0
  224. package/dist/src/formats/json/importer.d.ts +108 -0
  225. package/dist/src/formats/json/importer.d.ts.map +1 -0
  226. package/dist/src/formats/json/importer.js +1838 -0
  227. package/dist/src/formats/json/importer.js.map +1 -0
  228. package/dist/src/formats/json/index.d.ts +8 -0
  229. package/dist/src/formats/json/index.d.ts.map +1 -0
  230. package/dist/src/formats/json/index.js +8 -0
  231. package/dist/src/formats/json/index.js.map +1 -0
  232. package/dist/src/formats/neo4j/exporter.d.ts +68 -0
  233. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
  234. package/dist/src/formats/neo4j/exporter.js +1055 -0
  235. package/dist/src/formats/neo4j/exporter.js.map +1 -0
  236. package/dist/src/formats/neo4j/header.d.ts +52 -0
  237. package/dist/src/formats/neo4j/header.d.ts.map +1 -0
  238. package/dist/src/formats/neo4j/header.js +131 -0
  239. package/dist/src/formats/neo4j/header.js.map +1 -0
  240. package/dist/src/formats/neo4j/importer.d.ts +73 -0
  241. package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
  242. package/dist/src/formats/neo4j/importer.js +932 -0
  243. package/dist/src/formats/neo4j/importer.js.map +1 -0
  244. package/dist/src/formats/neo4j/index.d.ts +79 -0
  245. package/dist/src/formats/neo4j/index.d.ts.map +1 -0
  246. package/dist/src/formats/neo4j/index.js +83 -0
  247. package/dist/src/formats/neo4j/index.js.map +1 -0
  248. package/dist/src/formats/pajek/exporter.d.ts +58 -0
  249. package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
  250. package/dist/src/formats/pajek/exporter.js +825 -0
  251. package/dist/src/formats/pajek/exporter.js.map +1 -0
  252. package/dist/src/formats/pajek/importer.d.ts +88 -0
  253. package/dist/src/formats/pajek/importer.d.ts.map +1 -0
  254. package/dist/src/formats/pajek/importer.js +1047 -0
  255. package/dist/src/formats/pajek/importer.js.map +1 -0
  256. package/dist/src/formats/pajek/index.d.ts +7 -0
  257. package/dist/src/formats/pajek/index.d.ts.map +1 -0
  258. package/dist/src/formats/pajek/index.js +7 -0
  259. package/dist/src/formats/pajek/index.js.map +1 -0
  260. package/dist/src/formats/pajek/syntax.d.ts +112 -0
  261. package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
  262. package/dist/src/formats/pajek/syntax.js +269 -0
  263. package/dist/src/formats/pajek/syntax.js.map +1 -0
  264. package/dist/src/index.d.ts +35 -0
  265. package/dist/src/index.d.ts.map +1 -0
  266. package/dist/src/index.js +39 -0
  267. package/dist/src/index.js.map +1 -0
  268. package/dist/src/registry.d.ts +207 -0
  269. package/dist/src/registry.d.ts.map +1 -0
  270. package/dist/src/registry.js +481 -0
  271. package/dist/src/registry.js.map +1 -0
  272. package/dist/src/sniff.d.ts +104 -0
  273. package/dist/src/sniff.d.ts.map +1 -0
  274. package/dist/src/sniff.js +357 -0
  275. package/dist/src/sniff.js.map +1 -0
  276. package/dist/src/types.d.ts +238 -0
  277. package/dist/src/types.d.ts.map +1 -0
  278. package/dist/src/types.js +29 -0
  279. package/dist/src/types.js.map +1 -0
  280. package/dist/tsconfig.build.tsbuildinfo +1 -0
  281. package/package.json +122 -7
  282. package/src/children.ts +335 -0
  283. package/src/common/attributes.ts +520 -0
  284. package/src/common/codes.ts +153 -0
  285. package/src/common/declared-types.ts +374 -0
  286. package/src/common/direction.ts +518 -0
  287. package/src/common/escape.ts +231 -0
  288. package/src/common/export.ts +817 -0
  289. package/src/common/format.ts +111 -0
  290. package/src/common/ids.ts +176 -0
  291. package/src/common/input.ts +378 -0
  292. package/src/common/lists.ts +196 -0
  293. package/src/common/options.ts +377 -0
  294. package/src/common/report.ts +352 -0
  295. package/src/common/temporal.ts +302 -0
  296. package/src/common/text.ts +294 -0
  297. package/src/common/weights.ts +202 -0
  298. package/src/common/writer.ts +123 -0
  299. package/src/common/xml.ts +1053 -0
  300. package/src/formats/csv/exporter.ts +894 -0
  301. package/src/formats/csv/header.ts +172 -0
  302. package/src/formats/csv/importer.ts +1104 -0
  303. package/src/formats/csv/index.ts +88 -0
  304. package/src/formats/csv/records.ts +813 -0
  305. package/src/formats/csv/values.ts +224 -0
  306. package/src/formats/dot/exporter.ts +1014 -0
  307. package/src/formats/dot/importer.ts +1549 -0
  308. package/src/formats/dot/index.ts +7 -0
  309. package/src/formats/dot/names.ts +40 -0
  310. package/src/formats/dot/tokenizer.ts +384 -0
  311. package/src/formats/gexf/exporter.ts +1696 -0
  312. package/src/formats/gexf/importer.ts +2333 -0
  313. package/src/formats/gexf/index.ts +142 -0
  314. package/src/formats/gexf/schema.ts +361 -0
  315. package/src/formats/gml/exporter.ts +1404 -0
  316. package/src/formats/gml/importer.ts +1591 -0
  317. package/src/formats/gml/index.ts +128 -0
  318. package/src/formats/gml/syntax.ts +545 -0
  319. package/src/formats/graphml/constants.ts +225 -0
  320. package/src/formats/graphml/exporter.ts +1458 -0
  321. package/src/formats/graphml/importer.ts +2027 -0
  322. package/src/formats/graphml/index.ts +8 -0
  323. package/src/formats/graphml/tree.ts +318 -0
  324. package/src/formats/json/dialect.ts +317 -0
  325. package/src/formats/json/exporter.ts +1616 -0
  326. package/src/formats/json/importer.ts +2271 -0
  327. package/src/formats/json/index.ts +8 -0
  328. package/src/formats/neo4j/exporter.ts +1287 -0
  329. package/src/formats/neo4j/header.ts +156 -0
  330. package/src/formats/neo4j/importer.ts +1220 -0
  331. package/src/formats/neo4j/index.ts +116 -0
  332. package/src/formats/pajek/exporter.ts +1000 -0
  333. package/src/formats/pajek/importer.ts +1311 -0
  334. package/src/formats/pajek/index.ts +7 -0
  335. package/src/formats/pajek/syntax.ts +307 -0
  336. package/src/index.ts +244 -0
  337. package/src/registry.ts +617 -0
  338. package/src/sniff.ts +397 -0
  339. package/src/types.ts +262 -0
@@ -0,0 +1,1287 @@
1
+ /**
2
+ * The Neo4j exporter (design section 8.5, research note 07 section 9): writes a snapshot as
3
+ * neo4j-admin import CSV -- node sections with `:ID`, `:LABEL` and typed property columns,
4
+ * relationship sections with `:START_ID`, `:END_ID`, `:TYPE`, the weight and typed property
5
+ * columns -- as one document (node sections first) or as the node or relationship part alone.
6
+ *
7
+ * What a property graph cannot carry is reported by check() before anything is written: an
8
+ * undirected snapshot (every edge becomes a directed relationship), expanded mixed direction (per
9
+ * `onMixedDirection`), edge ids, hierarchy, element lifetimes, graph attributes, nested json (points
10
+ * excepted), multi-component columns (flattened to arrays), u32 / u8 columns (written as `long` /
11
+ * `int`), ids whose text would re-import as another type or collide, and list items that contain
12
+ * the array delimiter.
13
+ *
14
+ * Column headers restore the declared Neo4j type from `origin.type` when it is compatible with the
15
+ * column's dtype (`int`, `byte`, `short`, `long`, `char`, `duration`, the temporal types, `point`,
16
+ * `type[]`), and derive it from the dtype otherwise. Temporal columns write their `.text`
17
+ * companion when set and the canonical ISO form otherwise. Nodes are grouped into sections by
18
+ * (id space, stored-id column) in index order, so a re-import restores the same node order;
19
+ * relationships likewise by (start space, end space).
20
+ */
21
+
22
+ import {
23
+ type Column,
24
+ type ColumnMeta,
25
+ type Dtype,
26
+ GraphFormatError,
27
+ type GraphSnapshot,
28
+ type NodeId,
29
+ type ScalarDtype,
30
+ } from "@graphty/graph-format";
31
+
32
+ import { ID_TEXT_COLLISION_CODE, ID_TEXT_TYPE_CODE } from "../../common/codes.js";
33
+ import { type DeclaredTypeSpec, mapDeclaredType } from "../../common/declared-types.js";
34
+ import { type PairFolding, pairFolding } from "../../common/direction.js";
35
+ import { quoteCsvCell } from "../../common/escape.js";
36
+ import { capabilities, checkCapabilities, LOSS } from "../../common/export.js";
37
+ import { formatF32, formatF64, formatInteger } from "../../common/format.js";
38
+ import { isCanonicalIntegerText } from "../../common/ids.js";
39
+ import { type ResolvedExportOptions, resolveExportOptions } from "../../common/options.js";
40
+ import { formatTemporal, type TemporalKind } from "../../common/temporal.js";
41
+ import { type ExplicitWeights, explicitWeights } from "../../common/weights.js";
42
+ import { encodeChunks, joinText } from "../../common/writer.js";
43
+ import { type CommonExportOptions, type ExportCapabilities, type GraphExporter, type LossNote } from "../../types.js";
44
+ import { checkRecordSyntax, type RecordSyntax } from "../csv/records.js";
45
+ import { formatHeaderField } from "./header.js";
46
+ import { ID_SPACE_COLUMN, LABELS_COLUMN, TYPE_COLUMN } from "./importer.js";
47
+
48
+ /** The format-specific options of the Neo4j exporter. */
49
+ export interface Neo4jExportOptions {
50
+ /** Which tables to write: both (node sections first; default), the node sections or the relationship sections. */
51
+ part?: "all" | "nodes" | "relationships" | undefined;
52
+ /** The field delimiter; one character; "," by default. */
53
+ delimiter?: string | undefined;
54
+ /** The array delimiter of list values and `:LABEL` cells; ";" by default. */
55
+ arrayDelimiter?: ";" | "," | "|" | undefined;
56
+ /** The quote character; one character; a double quote by default. */
57
+ quote?: string | undefined;
58
+ /**
59
+ * The property that receives explicit edge weights (`<name>:double`); "weight" by default (the
60
+ * importer's `weightFrom` default); null writes no weights.
61
+ */
62
+ weightColumn?: string | null | undefined;
63
+ /**
64
+ * The property name of the `:ID` column for nodes that have no stored-id column of their own
65
+ * (`<name>:ID`); null (default) writes a bare `:ID`.
66
+ */
67
+ idColumn?: string | null | undefined;
68
+ }
69
+
70
+ /** Loss code: undirected edges (an undirected snapshot, or the folded pairs of a mixed one) written as directed relationships. */
71
+ export const UNDIRECTED_LOSS = "W_NEO4J_UNDIRECTED_AS_DIRECTED";
72
+
73
+ /** Loss code: node ids whose text re-imports as another type under the canonical id rule. */
74
+ export const ID_TEXT_TYPE_LOSS = ID_TEXT_TYPE_CODE;
75
+
76
+ /** Loss code: two node ids share one text; export() throws E_INVALID_ID. */
77
+ export const ID_TEXT_COLLISION_LOSS = ID_TEXT_COLLISION_CODE;
78
+
79
+ /** Loss code: an edge property column already uses the weight column name; export() throws E_COLUMN_EXISTS. */
80
+ export const WEIGHT_COLUMN_TAKEN_LOSS = "E_NEO4J_WEIGHT_COLUMN_TAKEN";
81
+
82
+ /** Loss code: a node property column already uses the idColumn name; export() throws E_COLUMN_EXISTS. */
83
+ export const ID_COLUMN_TAKEN_LOSS = "E_NEO4J_ID_COLUMN_TAKEN";
84
+
85
+ /** Loss code: a node has more than one stored-id column set; only the first is written. */
86
+ export const MULTIPLE_ID_PROPERTIES_LOSS = "W_NEO4J_MULTIPLE_ID_PROPERTIES";
87
+
88
+ /** Loss code: a declared integer type holds non-integral values and is written as double. */
89
+ export const DECLARED_TYPE_CHANGED_LOSS = "W_NEO4J_DECLARED_TYPE_CHANGED";
90
+
91
+ /** Loss code: a list item contains the array delimiter, which Neo4j cannot escape. */
92
+ export const ARRAY_DELIMITER_LOSS = "W_NEO4J_ARRAY_DELIMITER";
93
+
94
+ /** The roles Neo4j has a slot for: `:TYPE`, `:LABEL` and the id space of `:ID(Space)`. */
95
+ const SLOT_ROLES: ReadonlySet<string> = new Set(["kind", "labels", "idSpace"]);
96
+
97
+ const NEO4J = "neo4j";
98
+ const ID_TYPE = "ID";
99
+
100
+ /** The weight property the importer reads by default (its weightFrom default). */
101
+ const DEFAULT_WEIGHT_COLUMN = "weight";
102
+
103
+ /** The names the importer gives the slot columns, for the name-change notes. */
104
+ const ROLE_NAMES: Readonly<Record<string, string>> = Object.freeze({
105
+ kind: TYPE_COLUMN,
106
+ labels: LABELS_COLUMN,
107
+ idSpace: ID_SPACE_COLUMN,
108
+ });
109
+
110
+ /**
111
+ * The capabilities of neo4j-admin CSV (research note 07 section 9): declared scalar types and
112
+ * arrays; a dict column reads back as string (the header has no enumeration type); a position or
113
+ * visual column is written as a plain property (a point for a 2- or 3-component position) and
114
+ * reads back without its role.
115
+ */
116
+ export const NEO4J_CAPABILITIES: ExportCapabilities = capabilities({
117
+ mixedDirection: false,
118
+ multiEdges: true,
119
+ selfLoops: true,
120
+ edgeIds: "none",
121
+ idCharset: "any",
122
+ dtypes: ["f32", "f64", "i32", "bool", "string"],
123
+ components: false,
124
+ lists: true,
125
+ json: false,
126
+ defaults: false,
127
+ options: false,
128
+ hierarchy: false,
129
+ temporal: "none",
130
+ graphAttributes: false,
131
+ positions: false,
132
+ viz: false,
133
+ });
134
+
135
+ /** Roles whose columns are never written as properties. */
136
+ const SKIPPED_ROLES: ReadonlySet<string> = new Set([
137
+ "directed",
138
+ "pair",
139
+ "mutual",
140
+ "weight",
141
+ "timeText",
142
+ "originalId",
143
+ "parent",
144
+ "parents",
145
+ "start",
146
+ "end",
147
+ "timestamp",
148
+ "timestamps",
149
+ "spells",
150
+ "open",
151
+ ]);
152
+
153
+ /** Notes about relationships only, dropped when only nodes are written. */
154
+ const EDGE_NOTE_CODES: ReadonlySet<string> = new Set([
155
+ LOSS.MIXED_DIRECTION,
156
+ LOSS.MIXED_DIRECTION_ERROR,
157
+ LOSS.MULTI_EDGES,
158
+ LOSS.SELF_LOOPS,
159
+ LOSS.EDGE_IDS_GENERATED,
160
+ LOSS.EDGE_IDS_DROPPED,
161
+ UNDIRECTED_LOSS,
162
+ WEIGHT_COLUMN_TAKEN_LOSS,
163
+ ]);
164
+
165
+ /** Notes about nodes only, dropped when only relationships are written. */
166
+ const NODE_NOTE_CODES: ReadonlySet<string> = new Set([
167
+ LOSS.ID_MANGLED,
168
+ LOSS.ID_CHARSET,
169
+ LOSS.ID_RENUMBERED,
170
+ ID_TEXT_TYPE_LOSS,
171
+ ID_COLUMN_TAKEN_LOSS,
172
+ MULTIPLE_ID_PROPERTIES_LOSS,
173
+ ]);
174
+
175
+ const ARRAY_SYNTAXES: ReadonlySet<string> = new Set([";", ",", "|"]);
176
+ const PARTS: ReadonlySet<string> = new Set(["all", "nodes", "relationships"]);
177
+
178
+ /** The resolved format options. */
179
+ interface ResolvedNeo4jExportOptions {
180
+ readonly part: "all" | "nodes" | "relationships";
181
+ readonly syntax: RecordSyntax & { readonly delimiter: string };
182
+ readonly arrayDelimiter: string;
183
+ readonly weightColumn: string | null;
184
+ readonly idColumn: string | null;
185
+ }
186
+
187
+ /** How one cell's value is written. */
188
+ type CellKind = "integer" | "float" | "double" | "boolean" | "string" | "temporal" | "point" | "json";
189
+
190
+ /** The Neo4j type and cell kind of one scalar (a column or a list item). */
191
+ interface ScalarPlan {
192
+ readonly type: string;
193
+ readonly kind: CellKind;
194
+ readonly temporal: TemporalKind | null;
195
+ }
196
+
197
+ /** One property column as it will be written. */
198
+ interface ColumnPlan {
199
+ readonly column: Column;
200
+ readonly header: string;
201
+ readonly scalar: ScalarPlan;
202
+ /** A list (or flattened vector) column: items are joined by the array delimiter. */
203
+ readonly list: boolean;
204
+ /** The companion text column of a temporal column, or null. */
205
+ readonly companion: Column | null;
206
+ }
207
+
208
+ /**
209
+ * Resolve the format options.
210
+ * @param options - the caller's options
211
+ * @returns the resolved options; E_UNSUPPORTED for an invalid value
212
+ */
213
+ function resolveNeo4jExportOptions(options: Neo4jExportOptions | undefined): ResolvedNeo4jExportOptions {
214
+ const o: Neo4jExportOptions = options ?? {};
215
+ const part = o.part ?? "all";
216
+ if (!PARTS.has(part)) {
217
+ throw new GraphFormatError(
218
+ "E_UNSUPPORTED",
219
+ `option part: ${JSON.stringify(part)} is not "all", "nodes" or "relationships"`,
220
+ {
221
+ option: "part",
222
+ found: part,
223
+ },
224
+ );
225
+ }
226
+ const arrayDelimiter = o.arrayDelimiter ?? ";";
227
+ if (!ARRAY_SYNTAXES.has(arrayDelimiter)) {
228
+ throw new GraphFormatError(
229
+ "E_UNSUPPORTED",
230
+ `option arrayDelimiter: ${JSON.stringify(arrayDelimiter)} is not one of ";", ",", "|"`,
231
+ { option: "arrayDelimiter", found: arrayDelimiter },
232
+ );
233
+ }
234
+ const delimiter = o.delimiter ?? ",";
235
+ const syntax = { ...checkRecordSyntax({ delimiter, quote: o.quote ?? '"' }), delimiter };
236
+ if (syntax.delimiter === arrayDelimiter) {
237
+ throw new GraphFormatError("E_UNSUPPORTED", "options delimiter and arrayDelimiter must differ", {
238
+ option: "arrayDelimiter",
239
+ found: arrayDelimiter,
240
+ });
241
+ }
242
+ const weightColumn = o.weightColumn === undefined ? "weight" : o.weightColumn;
243
+ if (weightColumn !== null && (typeof weightColumn !== "string" || weightColumn.length === 0)) {
244
+ throw new GraphFormatError("E_UNSUPPORTED", "option weightColumn: expected a non-empty name or null", {
245
+ option: "weightColumn",
246
+ found: weightColumn,
247
+ });
248
+ }
249
+ const idColumn = o.idColumn ?? null;
250
+ if (idColumn !== null && (typeof idColumn !== "string" || idColumn.length === 0)) {
251
+ throw new GraphFormatError("E_UNSUPPORTED", "option idColumn: expected a non-empty name or null", {
252
+ option: "idColumn",
253
+ found: idColumn,
254
+ });
255
+ }
256
+ return { part, syntax, arrayDelimiter, weightColumn, idColumn };
257
+ }
258
+
259
+ /**
260
+ * Everything check() and export() share: the column plans, the section keys, the notes and the
261
+ * error export() throws (if any), computed once from the snapshot and the options.
262
+ */
263
+ class ExportPlan {
264
+ readonly snapshot: GraphSnapshot;
265
+
266
+ readonly options: ResolvedNeo4jExportOptions;
267
+
268
+ readonly common: ResolvedExportOptions;
269
+
270
+ /** The notes: the generic capability notes first, then the format's own, in that order. */
271
+ readonly notes: LossNote[] = [];
272
+
273
+ /** The format's own notes, appended after the generic ones. */
274
+ private readonly ownNotes: LossNote[] = [];
275
+
276
+ /** The error export() throws because of the node sections, or null. */
277
+ private fatalNodes: GraphFormatError | null = null;
278
+
279
+ /** The error export() throws because of the relationship sections, or null. */
280
+ private fatalEdges: GraphFormatError | null = null;
281
+
282
+ /** The error export() throws whatever the part (an id text collision), or null. */
283
+ private fatalAll: GraphFormatError | null = null;
284
+
285
+ readonly nodeColumns: ColumnPlan[] = [];
286
+
287
+ readonly edgeColumns: ColumnPlan[] = [];
288
+
289
+ /** Stored-id node columns (`name:ID`), in declaration order. */
290
+ readonly idColumns: Column[] = [];
291
+
292
+ readonly idSpace: Column | null;
293
+
294
+ readonly labels: Column | null;
295
+
296
+ readonly kind: Column | null;
297
+
298
+ readonly weights: ExplicitWeights | null;
299
+
300
+ /** The pair folding (design section 3.6): the mirror halves of undirected pairs are not written. */
301
+ readonly folding: PairFolding;
302
+
303
+ /**
304
+ * Build the plan.
305
+ * @param snapshot - the snapshot
306
+ * @param options - the resolved format options
307
+ * @param common - the resolved common options
308
+ */
309
+ constructor(snapshot: GraphSnapshot, options: ResolvedNeo4jExportOptions, common: ResolvedExportOptions) {
310
+ this.snapshot = snapshot;
311
+ this.options = options;
312
+ this.common = common;
313
+ this.idSpace = snapshot.nodes.byRole("idSpace");
314
+ this.labels = snapshot.nodes.byRole("labels");
315
+ this.kind = snapshot.edges.byRole("kind");
316
+ for (const column of snapshot.nodes) {
317
+ if (isStoredId(column.meta)) {
318
+ this.idColumns.push(column);
319
+ }
320
+ }
321
+ const generic = checkCapabilities(snapshot, NEO4J_CAPABILITIES, common, {
322
+ roles: SLOT_ROLES,
323
+ roleNames: ROLE_NAMES,
324
+ temporalText: true,
325
+ });
326
+ this.planColumns("node", snapshot.nodes, this.nodeColumns);
327
+ this.planColumns("edge", snapshot.edges, this.edgeColumns);
328
+ this.weights = this.planWeightSource();
329
+ this.folding = pairFolding(snapshot);
330
+ this.checkIds();
331
+ this.checkIdColumn();
332
+ this.checkStoredIds();
333
+ this.directionNotes();
334
+ const slots = new Set([this.kind, this.labels, this.idSpace].flatMap((c) => (c === null ? [] : [c.meta.name])));
335
+ for (const gen of generic) {
336
+ if (gen.code === LOSS.JSON && gen.column !== null && this.writtenAsPoint(gen.column)) {
337
+ continue;
338
+ }
339
+ if (gen.code === LOSS.DTYPE && gen.column !== null && slots.has(gen.column)) {
340
+ // :TYPE, :LABEL and :ID(Space) read back as the dict / list-of-dict columns they were
341
+ continue;
342
+ }
343
+ if (gen.code === LOSS.MIXED_DIRECTION_ERROR && this.fatalEdges === null) {
344
+ this.fatalEdges = new GraphFormatError("E_DIRECTED", gen.message, { reason: "mixed direction" });
345
+ }
346
+ if ((gen.code === LOSS.POSITIONS || gen.code === LOSS.VIZ) && gen.column !== null) {
347
+ // written as a plain property (a point for a position); the role is what is lost
348
+ this.notes.push(
349
+ Object.freeze({
350
+ code: gen.code,
351
+ message: `node column "${gen.column}" is written as a plain property; its role is lost on re-import`,
352
+ column: gen.column,
353
+ count: gen.count,
354
+ }),
355
+ );
356
+ continue;
357
+ }
358
+ this.notes.push(gen);
359
+ }
360
+ this.notes.push(...this.ownNotes);
361
+ this.filterNotesByPart();
362
+ }
363
+
364
+ /**
365
+ * The direction notes: an undirected snapshot writes every edge as a directed relationship;
366
+ * a mixed snapshot folds its undirected pairs to one directed relationship each under
367
+ * "directed" or "undirected" (Neo4j has no undirected relationship, so both policies write
368
+ * the same file and the generic W_MIXED_DIRECTION note names the policy); a mutual pair is
369
+ * written as two relationships without its mark.
370
+ */
371
+ private directionNotes(): void {
372
+ const { snapshot, folding } = this;
373
+ if (!snapshot.directed) {
374
+ this.note(
375
+ UNDIRECTED_LOSS,
376
+ `the snapshot is undirected; every edge is written as a directed relationship (${snapshot.edgeCount} edge(s))`,
377
+ null,
378
+ snapshot.edgeCount,
379
+ );
380
+ } else if (this.common.onMixedDirection !== "error") {
381
+ let undirected = 0;
382
+ for (let e = 0; e < snapshot.edgeCount; e++) {
383
+ if (!folding.folded(e) && !folding.sourceDirected(e)) {
384
+ undirected++;
385
+ }
386
+ }
387
+ if (undirected > 0) {
388
+ this.note(
389
+ UNDIRECTED_LOSS,
390
+ `${undirected} undirected edge(s) are written as one directed relationship each (source to target, a pair folded to its primary); Neo4j has no undirected relationship`,
391
+ null,
392
+ undirected,
393
+ );
394
+ }
395
+ }
396
+ if (folding.mutualCount > 0) {
397
+ this.note(
398
+ LOSS.MUTUAL_EXPANDED,
399
+ `${folding.mutualCount} mutual pair(s) are written as two directed relationships; the mutual mark is lost`,
400
+ null,
401
+ folding.mutualCount,
402
+ );
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Record a note.
408
+ * @param code - the code
409
+ * @param message - the message
410
+ * @param column - the column, or null
411
+ * @param count - the count, or null
412
+ */
413
+ private note(code: string, message: string, column: string | null = null, count: number | null = null): void {
414
+ this.ownNotes.push(Object.freeze({ code, message, column, count }));
415
+ }
416
+
417
+ /**
418
+ * Plan the property columns of one table.
419
+ * @param domain - node or edge
420
+ * @param table - the table
421
+ * @param out - receives the plans in declaration order
422
+ */
423
+ private planColumns(domain: "node" | "edge", table: Iterable<Column>, out: ColumnPlan[]): void {
424
+ const all = [...table];
425
+ const names = new Set(all.map((column) => column.meta.name));
426
+ const companionFor = new Map<string, Column>();
427
+ const companions = new Set<Column>();
428
+ for (const column of all) {
429
+ const target = column.meta.extra.for;
430
+ if (column.dtype === "string" && typeof target === "string" && names.has(target)) {
431
+ companionFor.set(target, column);
432
+ companions.add(column);
433
+ }
434
+ }
435
+ for (const column of all) {
436
+ const { meta } = column;
437
+ const { role } = meta;
438
+ if (companions.has(column) || (role !== null && SKIPPED_ROLES.has(role))) {
439
+ continue;
440
+ }
441
+ if (domain === "node" && (isStoredId(meta) || column === this.idSpace || column === this.labels)) {
442
+ continue;
443
+ }
444
+ if (domain === "edge" && (role === "id" || column === this.kind)) {
445
+ continue;
446
+ }
447
+ out.push(this.planColumn(column, companionFor.get(meta.name) ?? null));
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Plan one property column: its header type, cell kind and notes.
453
+ * @param column - the column
454
+ * @param companion - its text companion, or null
455
+ * @returns the plan
456
+ */
457
+ private planColumn(column: Column, companion: Column | null): ColumnPlan {
458
+ const { meta } = column;
459
+ const declared = meta.origin?.type ?? null;
460
+ const mapped = declared === null ? null : mapDeclaredType(NEO4J, declared, "f64");
461
+ const label = `${meta.domain} column "${meta.name}"`;
462
+ if (meta.dtype === "list") {
463
+ const itemDtype = meta.itemDtype ?? "string";
464
+ const compatible =
465
+ mapped !== null && mapped.list && mapped.itemDtype === itemDtype && mapped.kind !== "long";
466
+ let scalar = compatible
467
+ ? { type: declaredScalar(declared as string), kind: kindOf(mapped), temporal: mapped.temporal }
468
+ : defaultScalar(itemDtype);
469
+ if (mapped !== null && mapped.list && mapped.kind === "long" && itemDtype === "f64") {
470
+ scalar = this.integralOrDouble(column, declaredScalar(declared as string), label);
471
+ }
472
+ const itemComponents = meta.itemComponents ?? 1;
473
+ if (itemComponents > 1) {
474
+ this.note(
475
+ LOSS.COMPONENTS,
476
+ `${label} holds items of ${itemComponents} components; they are flattened into one array`,
477
+ meta.name,
478
+ column.length - column.nullCount,
479
+ );
480
+ }
481
+ this.checkArrayItems(column, scalar, label);
482
+ return {
483
+ column,
484
+ header: formatHeaderField(meta.name, `${scalar.type}[]`, null),
485
+ scalar,
486
+ list: true,
487
+ companion,
488
+ };
489
+ }
490
+ let scalar: ScalarPlan;
491
+ const compatible = mapped !== null && !mapped.list && mapped.dtype === meta.dtype;
492
+ if (compatible && mapped.kind === "long") {
493
+ scalar = this.integralOrDouble(column, declared as string, label);
494
+ } else if (compatible) {
495
+ scalar = { type: declared as string, kind: kindOf(mapped), temporal: mapped.temporal };
496
+ } else {
497
+ scalar = defaultScalar(meta.dtype);
498
+ }
499
+ // the generic check treats position and visual columns by role only; their dtype and
500
+ // stride are checked here so the notes match those of any other column
501
+ const visual = meta.role === "position" || (meta.role !== null && isVizRole(meta.role));
502
+ if (visual && meta.dtype === "json" && scalar.kind !== "point") {
503
+ this.note(
504
+ LOSS.JSON,
505
+ `${label} holds nested values; the format has no nested values`,
506
+ meta.name,
507
+ column.length - column.nullCount,
508
+ );
509
+ } else if (visual && meta.dtype !== "json" && !NEO4J_CAPABILITIES.dtypes.includes(meta.dtype)) {
510
+ this.note(
511
+ LOSS.DTYPE,
512
+ `${label} is ${meta.dtype}; the format cannot keep that dtype`,
513
+ meta.name,
514
+ column.length - column.nullCount,
515
+ );
516
+ }
517
+ if (meta.components > 1) {
518
+ if (visual) {
519
+ this.note(
520
+ LOSS.COMPONENTS,
521
+ `${label} has ${meta.components} components; the format has no strides`,
522
+ meta.name,
523
+ column.length - column.nullCount,
524
+ );
525
+ }
526
+ return {
527
+ column,
528
+ header: formatHeaderField(meta.name, `${scalar.type}[]`, null),
529
+ scalar,
530
+ list: true,
531
+ companion,
532
+ };
533
+ }
534
+ // an untyped Neo4j header (`name` alone) is a string property; restore it as written
535
+ const untyped = declared === null && meta.origin?.format === NEO4J && meta.dtype === "string";
536
+ const header = untyped ? meta.name : formatHeaderField(meta.name, scalar.type, null);
537
+ return { column, header, scalar, list: false, companion };
538
+ }
539
+
540
+ /**
541
+ * The plan of an f64 column declared with an integer type: kept when every set value is
542
+ * integral, written as double (with a note) otherwise.
543
+ * @param column - the column
544
+ * @param declared - the declared type text
545
+ * @param label - the column label for messages
546
+ * @returns the scalar plan
547
+ */
548
+ private integralOrDouble(column: Column, declared: string, label: string): ScalarPlan {
549
+ let integral = true;
550
+ if (column.dtype === "f64") {
551
+ for (let r = 0; r < column.length && integral; r++) {
552
+ if (column.isSet(r) && !Number.isInteger(column.data[r])) {
553
+ integral = false;
554
+ }
555
+ }
556
+ } else if (column.dtype === "list" && column.child.dtype === "f64") {
557
+ const { data } = column.child;
558
+ for (let i = 0; i < data.length && integral; i++) {
559
+ if (!Number.isInteger(data[i])) {
560
+ integral = false;
561
+ }
562
+ }
563
+ }
564
+ if (integral) {
565
+ return { type: declared, kind: "integer", temporal: null };
566
+ }
567
+ this.note(
568
+ DECLARED_TYPE_CHANGED_LOSS,
569
+ `${label} is declared ${declared} but holds non-integral values; written as double`,
570
+ column.meta.name,
571
+ null,
572
+ );
573
+ return { type: "double", kind: "double", temporal: null };
574
+ }
575
+
576
+ /**
577
+ * Count the rows of a list column with an item containing the array delimiter.
578
+ * @param column - the list column
579
+ * @param scalar - the item plan
580
+ * @param label - the column label
581
+ */
582
+ private checkArrayItems(column: Column, scalar: ScalarPlan, label: string): void {
583
+ if (
584
+ column.dtype !== "list" ||
585
+ (scalar.kind !== "string" && scalar.kind !== "json" && scalar.kind !== "point")
586
+ ) {
587
+ return;
588
+ }
589
+ const { arrayDelimiter } = this.options;
590
+ let rows = 0;
591
+ for (let r = 0; r < column.length; r++) {
592
+ if (!column.isSet(r)) {
593
+ continue;
594
+ }
595
+ for (const item of column.sliceOf(r)) {
596
+ if (formatScalar(item, scalar, null).includes(arrayDelimiter)) {
597
+ rows++;
598
+ break;
599
+ }
600
+ }
601
+ }
602
+ if (rows > 0) {
603
+ this.note(
604
+ ARRAY_DELIMITER_LOSS,
605
+ `${label}: ${rows} row(s) hold an item containing the array delimiter "${arrayDelimiter}", which Neo4j cannot escape`,
606
+ column.meta.name,
607
+ rows,
608
+ );
609
+ }
610
+ }
611
+
612
+ /**
613
+ * The explicit weights (design section 3.7), and the weight-column name check.
614
+ * @returns the weights, or null when no weights are written
615
+ */
616
+ private planWeightSource(): ExplicitWeights | null {
617
+ const { snapshot } = this;
618
+ const name = this.options.weightColumn;
619
+ if (name === null) {
620
+ return null;
621
+ }
622
+ const weights = explicitWeights(snapshot);
623
+ const taken = this.edgeColumns.find((plan) => plan.column.meta.name === name);
624
+ if (taken !== undefined && weights.weighted) {
625
+ this.note(
626
+ WEIGHT_COLUMN_TAKEN_LOSS,
627
+ `edge column "${name}" already exists; explicit weights cannot be written under weightColumn "${name}"`,
628
+ name,
629
+ null,
630
+ );
631
+ if (this.fatalEdges === null) {
632
+ this.fatalEdges = new GraphFormatError(
633
+ "E_COLUMN_EXISTS",
634
+ `edge column "${name}" already exists; choose another weightColumn`,
635
+ { column: name, domain: "edge" },
636
+ );
637
+ }
638
+ } else if (taken !== undefined && name === DEFAULT_WEIGHT_COLUMN) {
639
+ this.note(
640
+ LOSS.WEIGHT_KEY_CLASH,
641
+ `edge column "${name}" is written under the property the importer reads THE weight from (weightFrom "${name}"); it reads back as the weight, not as a column`,
642
+ name,
643
+ taken.column.length - taken.column.nullCount,
644
+ );
645
+ }
646
+ return weights.weighted ? weights : null;
647
+ }
648
+
649
+ /** Check the node ids' text forms: type changes under the canonical rule and collisions. */
650
+ private checkIds(): void {
651
+ const { ids } = this.snapshot;
652
+ let typeChanges = 0;
653
+ let collisions = 0;
654
+ switch (ids.kind) {
655
+ case "identity":
656
+ case "dense":
657
+ break;
658
+ case "numeric":
659
+ for (let i = 0; i < ids.size; i++) {
660
+ if (!Number.isSafeInteger(ids.idOf(i))) {
661
+ typeChanges++;
662
+ }
663
+ }
664
+ break;
665
+ case "string":
666
+ for (let i = 0; i < ids.size; i++) {
667
+ if (isCanonicalIntegerText(String(ids.idOf(i)))) {
668
+ typeChanges++;
669
+ }
670
+ }
671
+ break;
672
+ case "mixed": {
673
+ const seen = new Set<string>();
674
+ for (let i = 0; i < ids.size; i++) {
675
+ const id = ids.idOf(i);
676
+ const text = String(id);
677
+ if (typeof id === "number" ? !Number.isSafeInteger(id) : isCanonicalIntegerText(text)) {
678
+ typeChanges++;
679
+ }
680
+ if (seen.has(text)) {
681
+ collisions++;
682
+ } else {
683
+ seen.add(text);
684
+ }
685
+ }
686
+ break;
687
+ }
688
+ default: {
689
+ const name: string = ids.kind;
690
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${name}`, { kind: name });
691
+ }
692
+ }
693
+ if (typeChanges > 0) {
694
+ this.note(
695
+ ID_TEXT_TYPE_LOSS,
696
+ `${typeChanges} node id(s) re-import as another type under the canonical id rule (a string of canonical integer text, or a non-integer number)`,
697
+ null,
698
+ typeChanges,
699
+ );
700
+ }
701
+ if (collisions > 0) {
702
+ this.note(
703
+ ID_TEXT_COLLISION_LOSS,
704
+ `${collisions} node id(s) share their text with another id (a number and a string); export() will throw`,
705
+ null,
706
+ collisions,
707
+ );
708
+ this.fatalAll = new GraphFormatError(
709
+ "E_INVALID_ID",
710
+ `${collisions} node id(s) share their text with another id; Neo4j ids are text`,
711
+ { reason: "text collision", count: collisions },
712
+ );
713
+ }
714
+ }
715
+
716
+ /** Check that the idColumn option does not name an existing node property. */
717
+ private checkIdColumn(): void {
718
+ const name = this.options.idColumn;
719
+ if (name === null) {
720
+ return;
721
+ }
722
+ const taken =
723
+ this.nodeColumns.some((plan) => plan.column.meta.name === name) ||
724
+ this.idColumns.some((column) => column.meta.name === name);
725
+ if (!taken) {
726
+ return;
727
+ }
728
+ this.note(
729
+ ID_COLUMN_TAKEN_LOSS,
730
+ `node column "${name}" already exists; the :ID column cannot be named idColumn "${name}"`,
731
+ name,
732
+ null,
733
+ );
734
+ if (this.fatalNodes === null) {
735
+ this.fatalNodes = new GraphFormatError(
736
+ "E_COLUMN_EXISTS",
737
+ `node column "${name}" already exists; choose another idColumn`,
738
+ { column: name, domain: "node" },
739
+ );
740
+ }
741
+ }
742
+
743
+ /** Count the nodes with more than one stored-id column set. */
744
+ private checkStoredIds(): void {
745
+ if (this.idColumns.length < 2) {
746
+ return;
747
+ }
748
+ let count = 0;
749
+ for (let i = 0; i < this.snapshot.nodeCount; i++) {
750
+ let set = 0;
751
+ for (const column of this.idColumns) {
752
+ if (column.isSet(i)) {
753
+ set++;
754
+ }
755
+ }
756
+ if (set > 1) {
757
+ count++;
758
+ }
759
+ }
760
+ if (count > 0) {
761
+ this.note(
762
+ MULTIPLE_ID_PROPERTIES_LOSS,
763
+ `${count} node(s) have more than one stored-id column set; only the first in declaration order is written`,
764
+ null,
765
+ count,
766
+ );
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Whether a json column is written as Neo4j points (so the generic nested-value note does not apply).
772
+ * @param name - the column name
773
+ * @returns true when its plan writes point literals
774
+ */
775
+ private writtenAsPoint(name: string): boolean {
776
+ for (const plan of [...this.nodeColumns, ...this.edgeColumns]) {
777
+ if (plan.column.meta.name === name && plan.scalar.kind === "point") {
778
+ return true;
779
+ }
780
+ }
781
+ return false;
782
+ }
783
+
784
+ /** Drop the notes about the part that is not written. */
785
+ private filterNotesByPart(): void {
786
+ const { part } = this.options;
787
+ if (part === "all") {
788
+ return;
789
+ }
790
+ const { snapshot } = this;
791
+ const keep = this.notes.filter((n) => {
792
+ if (part === "nodes") {
793
+ if (EDGE_NOTE_CODES.has(n.code)) {
794
+ return false;
795
+ }
796
+ return n.column === null || !snapshot.edges.has(n.column) || snapshot.nodes.has(n.column);
797
+ }
798
+ if (NODE_NOTE_CODES.has(n.code)) {
799
+ return false;
800
+ }
801
+ return n.column === null || !snapshot.nodes.has(n.column) || snapshot.edges.has(n.column);
802
+ });
803
+ this.notes.length = 0;
804
+ this.notes.push(...keep);
805
+ }
806
+
807
+ /**
808
+ * The error export() throws for the requested part, or null when the export can proceed.
809
+ * @returns the error
810
+ */
811
+ get fatal(): GraphFormatError | null {
812
+ if (this.fatalAll !== null) {
813
+ return this.fatalAll;
814
+ }
815
+ const { part } = this.options;
816
+ if (part !== "relationships" && this.fatalNodes !== null) {
817
+ return this.fatalNodes;
818
+ }
819
+ return part === "nodes" ? null : this.fatalEdges;
820
+ }
821
+
822
+ /**
823
+ * The id space of a node.
824
+ * @param index - the node index
825
+ * @returns the space name, or null
826
+ */
827
+ spaceOf(index: number): string | null {
828
+ const { idSpace } = this;
829
+ if (idSpace === null || !idSpace.isSet(index)) {
830
+ return null;
831
+ }
832
+ const value = idSpace.value(index);
833
+ return typeof value === "string" ? value : String(value);
834
+ }
835
+
836
+ /**
837
+ * The stored-id column of a node: the first one set in declaration order.
838
+ * @param index - the node index
839
+ * @returns the column, or null
840
+ */
841
+ storedIdOf(index: number): Column | null {
842
+ for (const column of this.idColumns) {
843
+ if (column.isSet(index)) {
844
+ return column;
845
+ }
846
+ }
847
+ return null;
848
+ }
849
+
850
+ /**
851
+ * The node sections, then the relationship sections, as CSV lines.
852
+ * @yields one line at a time (with its line break)
853
+ * @returns nothing
854
+ */
855
+ *lines(): Generator<string, void, undefined> {
856
+ if (this.fatal !== null) {
857
+ throw this.fatal;
858
+ }
859
+ const { part } = this.options;
860
+ if (part !== "relationships") {
861
+ yield* this.nodeLines();
862
+ }
863
+ if (part !== "nodes") {
864
+ yield* this.relationshipLines();
865
+ }
866
+ }
867
+
868
+ /**
869
+ * The node sections.
870
+ * @yields one line at a time
871
+ * @returns nothing
872
+ */
873
+ private *nodeLines(): Generator<string, void, undefined> {
874
+ const { snapshot, labels, nodeColumns } = this;
875
+ const { ids } = snapshot;
876
+ const { delimiter } = this.options.syntax;
877
+ const propertyHeaders = nodeColumns.map((plan) => plan.header).join(delimiter);
878
+ let sections = 0;
879
+ let sectionSpace: string | null = null;
880
+ let sectionIdName: string | null = null;
881
+ const cells: string[] = [];
882
+ for (let i = 0; i < snapshot.nodeCount; i++) {
883
+ const space = this.spaceOf(i);
884
+ const stored = this.storedIdOf(i);
885
+ const idName = stored === null ? (this.options.idColumn ?? "") : stored.meta.name;
886
+ if (sections === 0 || space !== sectionSpace || idName !== sectionIdName) {
887
+ sections++;
888
+ sectionSpace = space;
889
+ sectionIdName = idName;
890
+ const header = [formatHeaderField(idName, ID_TYPE, space)];
891
+ if (labels !== null) {
892
+ header.push(":LABEL");
893
+ }
894
+ if (propertyHeaders.length > 0) {
895
+ header.push(propertyHeaders);
896
+ }
897
+ yield `${header.join(delimiter)}\n`;
898
+ }
899
+ cells.length = 0;
900
+ cells.push(this.cell(idText(ids.idOf(i))));
901
+ if (labels !== null) {
902
+ cells.push(this.cell(this.labelsText(i)));
903
+ }
904
+ for (const plan of nodeColumns) {
905
+ cells.push(this.cell(this.valueText(plan, i)));
906
+ }
907
+ yield `${cells.join(delimiter)}\n`;
908
+ }
909
+ if (sections === 0) {
910
+ // no nodes: one header so the section (and its columns) still exists
911
+ const header = [formatHeaderField(this.options.idColumn ?? "", ID_TYPE, null)];
912
+ if (labels !== null) {
913
+ header.push(":LABEL");
914
+ }
915
+ if (propertyHeaders.length > 0) {
916
+ header.push(propertyHeaders);
917
+ }
918
+ yield `${header.join(delimiter)}\n`;
919
+ }
920
+ }
921
+
922
+ /**
923
+ * The relationship sections.
924
+ * @yields one line at a time
925
+ * @returns nothing
926
+ */
927
+ private *relationshipLines(): Generator<string, void, undefined> {
928
+ const { snapshot, kind, weights, edgeColumns, folding } = this;
929
+ const { ids } = snapshot;
930
+ const { delimiter } = this.options.syntax;
931
+ const list = snapshot.edgeList();
932
+ const propertyHeaders = edgeColumns.map((plan) => plan.header).join(delimiter);
933
+ const weightHeader =
934
+ weights === null ? null : formatHeaderField(this.options.weightColumn ?? "weight", "double", null);
935
+ let sections = 0;
936
+ let sectionStart: string | null = null;
937
+ let sectionEnd: string | null = null;
938
+ const cells: string[] = [];
939
+ const headerOf = (startSpace: string | null, endSpace: string | null): string => {
940
+ const header = [formatHeaderField("", "START_ID", startSpace), formatHeaderField("", "END_ID", endSpace)];
941
+ if (kind !== null) {
942
+ header.push(":TYPE");
943
+ }
944
+ if (weightHeader !== null) {
945
+ header.push(weightHeader);
946
+ }
947
+ if (propertyHeaders.length > 0) {
948
+ header.push(propertyHeaders);
949
+ }
950
+ return `${header.join(delimiter)}\n`;
951
+ };
952
+ for (let e = 0; e < snapshot.edgeCount; e++) {
953
+ if (folding.folded(e)) {
954
+ continue;
955
+ }
956
+ const u = list.src[e];
957
+ const v = list.dst[e];
958
+ const startSpace = this.spaceOf(u);
959
+ const endSpace = this.spaceOf(v);
960
+ if (sections === 0 || startSpace !== sectionStart || endSpace !== sectionEnd) {
961
+ sections++;
962
+ sectionStart = startSpace;
963
+ sectionEnd = endSpace;
964
+ yield headerOf(startSpace, endSpace);
965
+ }
966
+ cells.length = 0;
967
+ cells.push(this.cell(idText(ids.idOf(u))), this.cell(idText(ids.idOf(v))));
968
+ if (kind !== null) {
969
+ cells.push(this.cell(kind.isSet(e) ? textOf(kind, e) : null));
970
+ }
971
+ if (weights !== null) {
972
+ cells.push(this.cell(weights.text(e)));
973
+ }
974
+ for (const plan of edgeColumns) {
975
+ cells.push(this.cell(this.valueText(plan, e)));
976
+ }
977
+ yield `${cells.join(delimiter)}\n`;
978
+ }
979
+ if (sections === 0) {
980
+ yield headerOf(null, null);
981
+ }
982
+ }
983
+
984
+ /**
985
+ * A CSV cell: empty for an unset value, a quoted empty string for a set empty string, the
986
+ * quoted text otherwise.
987
+ * @param text - the value text, or null for unset
988
+ * @returns the cell as written
989
+ */
990
+ private cell(text: string | null): string {
991
+ if (text === null) {
992
+ return "";
993
+ }
994
+ const { quote } = this.options.syntax;
995
+ if (text.length === 0) {
996
+ return quote + quote;
997
+ }
998
+ return quoteCsvCell(text, this.options.syntax.delimiter);
999
+ }
1000
+
1001
+ /**
1002
+ * The `:LABEL` cell of a node.
1003
+ * @param index - the node index
1004
+ * @returns the labels joined by the array delimiter, or null when unset
1005
+ */
1006
+ private labelsText(index: number): string | null {
1007
+ const { labels } = this;
1008
+ if (labels === null || !labels.isSet(index)) {
1009
+ return null;
1010
+ }
1011
+ if (labels.dtype === "list") {
1012
+ return labels
1013
+ .sliceOf(index)
1014
+ .map((item) => String(item))
1015
+ .join(this.options.arrayDelimiter);
1016
+ }
1017
+ return textOf(labels, index);
1018
+ }
1019
+
1020
+ /**
1021
+ * The text of one property cell.
1022
+ * @param plan - the column plan
1023
+ * @param row - the row
1024
+ * @returns the text, or null when unset
1025
+ */
1026
+ private valueText(plan: ColumnPlan, row: number): string | null {
1027
+ const { column, scalar, companion } = plan;
1028
+ if (!column.isSet(row)) {
1029
+ return null;
1030
+ }
1031
+ if (plan.list) {
1032
+ const items = column.dtype === "list" ? column.sliceOf(row) : (column.value(row) as ArrayLike<unknown>);
1033
+ const texts: string[] = [];
1034
+ for (const item of Array.from(items)) {
1035
+ // an item of several components (itemComponents > 1) is flattened into the array
1036
+ if (Array.isArray(item) || ArrayBuffer.isView(item)) {
1037
+ for (const component of Array.from(item as ArrayLike<unknown>)) {
1038
+ texts.push(formatScalar(component, scalar, null));
1039
+ }
1040
+ } else {
1041
+ texts.push(formatScalar(item, scalar, null));
1042
+ }
1043
+ }
1044
+ return texts.join(this.options.arrayDelimiter);
1045
+ }
1046
+ const text = companion !== null && companion.isSet(row) ? textOf(companion, row) : null;
1047
+ return formatScalar(column.dtype === "json" ? column.values[row] : column.value(row), scalar, text);
1048
+ }
1049
+ }
1050
+
1051
+ /**
1052
+ * Whether a node column is a stored id (`name:ID` on import).
1053
+ * @param meta - the column metadata
1054
+ * @returns true for a string column declared by the Neo4j importer as the id property
1055
+ */
1056
+ function isStoredId(meta: ColumnMeta): boolean {
1057
+ return (
1058
+ meta.role === null &&
1059
+ meta.dtype === "string" &&
1060
+ meta.origin !== null &&
1061
+ meta.origin.format === NEO4J &&
1062
+ meta.origin.type === ID_TYPE
1063
+ );
1064
+ }
1065
+
1066
+ /**
1067
+ * Whether a role is one of the visual roles.
1068
+ * @param role - the role
1069
+ * @returns true for color, size, shape and thickness
1070
+ */
1071
+ function isVizRole(role: string): boolean {
1072
+ return role === "color" || role === "size" || role === "shape" || role === "thickness";
1073
+ }
1074
+
1075
+ /**
1076
+ * The declared scalar type text of a list declaration (`string[]` -> `string`).
1077
+ * @param declared - the declared type text
1078
+ * @returns the text without its `[]` suffix
1079
+ */
1080
+ function declaredScalar(declared: string): string {
1081
+ const trimmed = declared.trim();
1082
+ return trimmed.endsWith("[]") ? trimmed.slice(0, -2) : trimmed;
1083
+ }
1084
+
1085
+ /**
1086
+ * The cell kind of a mapped declared type.
1087
+ * @param spec - the mapped type
1088
+ * @returns the kind
1089
+ */
1090
+ function kindOf(spec: DeclaredTypeSpec): CellKind {
1091
+ switch (spec.kind) {
1092
+ case "boolean":
1093
+ return "boolean";
1094
+ case "integer":
1095
+ case "long":
1096
+ return "integer";
1097
+ case "float":
1098
+ return "float";
1099
+ case "double":
1100
+ return "double";
1101
+ case "string":
1102
+ case "duration":
1103
+ return "string";
1104
+ case "temporal":
1105
+ return "temporal";
1106
+ case "point":
1107
+ return "point";
1108
+ case "json":
1109
+ return "json";
1110
+ default: {
1111
+ const name: string = spec.kind;
1112
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown value kind ${name}`, { kind: name });
1113
+ }
1114
+ }
1115
+ }
1116
+
1117
+ /**
1118
+ * The Neo4j type of a dtype without a compatible declaration.
1119
+ * @param dtype - the column or item dtype
1120
+ * @returns the type and cell kind
1121
+ */
1122
+ function defaultScalar(dtype: Dtype | ScalarDtype): ScalarPlan {
1123
+ switch (dtype) {
1124
+ case "f32":
1125
+ return { type: "float", kind: "float", temporal: null };
1126
+ case "f64":
1127
+ return { type: "double", kind: "double", temporal: null };
1128
+ case "i32":
1129
+ case "u8":
1130
+ return { type: "int", kind: "integer", temporal: null };
1131
+ case "u32":
1132
+ return { type: "long", kind: "integer", temporal: null };
1133
+ case "bool":
1134
+ return { type: "boolean", kind: "boolean", temporal: null };
1135
+ case "dict":
1136
+ case "string":
1137
+ return { type: "string", kind: "string", temporal: null };
1138
+ case "json":
1139
+ return { type: "string", kind: "json", temporal: null };
1140
+ case "list":
1141
+ return { type: "string", kind: "json", temporal: null };
1142
+ default: {
1143
+ const name: string = dtype;
1144
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${name}`, { dtype: name });
1145
+ }
1146
+ }
1147
+ }
1148
+
1149
+ /**
1150
+ * Format one scalar value for a cell.
1151
+ * @param value - the value
1152
+ * @param scalar - the plan
1153
+ * @param text - the companion text of a temporal value, or null
1154
+ * @returns the text
1155
+ */
1156
+ function formatScalar(value: unknown, scalar: ScalarPlan, text: string | null): string {
1157
+ switch (scalar.kind) {
1158
+ case "integer":
1159
+ return typeof value === "number" ? formatInteger(value) : String(value);
1160
+ case "float":
1161
+ return typeof value === "number" ? formatF32(value) : String(value);
1162
+ case "double":
1163
+ return typeof value === "number" ? formatF64(value) : String(value);
1164
+ case "boolean":
1165
+ return value === true ? "true" : "false";
1166
+ case "string":
1167
+ return typeof value === "string" ? value : String(value);
1168
+ case "temporal":
1169
+ if (text !== null) {
1170
+ return text;
1171
+ }
1172
+ return typeof value === "number" && scalar.temporal !== null
1173
+ ? formatTemporal(value, scalar.temporal)
1174
+ : String(value);
1175
+ case "point":
1176
+ return formatPoint(value);
1177
+ case "json":
1178
+ return JSON.stringify(value) ?? "";
1179
+ default: {
1180
+ const name: string = scalar.kind;
1181
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown cell kind ${name}`, { kind: name });
1182
+ }
1183
+ }
1184
+ }
1185
+
1186
+ /**
1187
+ * A Neo4j point literal `{x:1.5, y:2, crs:'cartesian'}` from a point object; any other value is
1188
+ * written as JSON text.
1189
+ * @param value - the json value
1190
+ * @returns the literal
1191
+ */
1192
+ function formatPoint(value: unknown): string {
1193
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1194
+ return JSON.stringify(value) ?? "";
1195
+ }
1196
+ const parts: string[] = [];
1197
+ for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
1198
+ if (typeof item === "number") {
1199
+ parts.push(`${key}:${formatF64(item)}`);
1200
+ } else if (typeof item === "string") {
1201
+ parts.push(`${key}:'${item.replace(/'/g, "")}'`);
1202
+ } else {
1203
+ return JSON.stringify(value) ?? "";
1204
+ }
1205
+ }
1206
+ return `{${parts.join(", ")}}`;
1207
+ }
1208
+
1209
+ /**
1210
+ * The text of a node id.
1211
+ * @param id - the id
1212
+ * @returns String(id)
1213
+ */
1214
+ function idText(id: NodeId): string {
1215
+ if (typeof id === "string") {
1216
+ return id;
1217
+ }
1218
+ return Number.isInteger(id) ? formatInteger(id) : String(id);
1219
+ }
1220
+
1221
+ /**
1222
+ * The text of a set cell of a dict / string (or any scalar) column.
1223
+ * @param column - the column
1224
+ * @param row - the row
1225
+ * @returns the value as text
1226
+ */
1227
+ function textOf(column: Column, row: number): string {
1228
+ switch (column.dtype) {
1229
+ case "string":
1230
+ return column.valueAt(row);
1231
+ case "dict":
1232
+ return String(column.value(row));
1233
+ case "list":
1234
+ return column.sliceOf(row).map(String).join(",");
1235
+ case "json":
1236
+ return JSON.stringify(column.values[row]) ?? "";
1237
+ case "bool":
1238
+ return column.value(row) === true ? "true" : "false";
1239
+ default: {
1240
+ const value = column.value(row);
1241
+ return typeof value === "number" ? String(value) : Array.from(value as ArrayLike<number>).join(",");
1242
+ }
1243
+ }
1244
+ }
1245
+
1246
+ /** The Neo4j exporter. */
1247
+ export const neo4jExporter: GraphExporter<Neo4jExportOptions> = Object.freeze({
1248
+ format: NEO4J,
1249
+ capabilities: NEO4J_CAPABILITIES,
1250
+ /**
1251
+ * Pre-flight: what export() would lose.
1252
+ * @param snapshot - the snapshot
1253
+ * @param options - format-specific and common options
1254
+ * @returns the notes
1255
+ */
1256
+ check(snapshot: GraphSnapshot, options?: Neo4jExportOptions & CommonExportOptions): readonly LossNote[] {
1257
+ return Object.freeze([...plan(snapshot, options).notes]);
1258
+ },
1259
+ /**
1260
+ * Write the snapshot as UTF-8 chunks.
1261
+ * @param snapshot - the snapshot
1262
+ * @param options - format-specific and common options
1263
+ * @returns the chunks
1264
+ */
1265
+ export(snapshot: GraphSnapshot, options?: Neo4jExportOptions & CommonExportOptions): AsyncIterable<Uint8Array> {
1266
+ return encodeChunks(plan(snapshot, options).lines());
1267
+ },
1268
+ /**
1269
+ * Write the snapshot as one string.
1270
+ * @param snapshot - the snapshot
1271
+ * @param options - format-specific and common options
1272
+ * @returns the document
1273
+ */
1274
+ exportToString(snapshot: GraphSnapshot, options?: Neo4jExportOptions & CommonExportOptions): Promise<string> {
1275
+ return joinText(plan(snapshot, options).lines());
1276
+ },
1277
+ });
1278
+
1279
+ /**
1280
+ * Build the export plan of a snapshot.
1281
+ * @param snapshot - the snapshot
1282
+ * @param options - the caller's options
1283
+ * @returns the plan
1284
+ */
1285
+ function plan(snapshot: GraphSnapshot, options: (Neo4jExportOptions & CommonExportOptions) | undefined): ExportPlan {
1286
+ return new ExportPlan(snapshot, resolveNeo4jExportOptions(options), resolveExportOptions(options));
1287
+ }