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