003_migrate_v1_format.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. /**
  2. * 003_migrate_v1_format.js
  3. *
  4. * Migrates nodes from V1 format to V2 format.
  5. *
  6. * V1 format (old):
  7. * label: "example.com"
  8. * type: "domain"
  9. * created_at: "2026-01-23T18:28:46.048223+00:00"
  10. * domain: "example.com"
  11. * root: true
  12. * sketch_id: "..."
  13. * x, y: coordinates
  14. *
  15. * V2 format (new):
  16. * nodeLabel: "example.com"
  17. * nodeType: "domain"
  18. * nodeMetadata.created_at: "2026-01-23T18:28:46.048223+00:00"
  19. * nodeProperties.domain: "example.com"
  20. * nodeProperties.root: true
  21. * sketch_id: "..."
  22. * x, y: coordinates
  23. *
  24. * This migration is IDEMPOTENT:
  25. * - Only processes nodes that have V1 format (have `label` or `type` but NOT `nodeLabel`)
  26. * - Safe to run multiple times
  27. * - Processes in batches to handle large datasets
  28. */
  29. import neo4j from "neo4j-driver";
  30. // Reserved properties that should NOT be moved to nodeProperties
  31. const RESERVED_PROPERTIES = new Set([
  32. "id",
  33. "x",
  34. "y",
  35. "nodeLabel",
  36. "label",
  37. "nodeType",
  38. "type",
  39. "nodeImage",
  40. "nodeIcon",
  41. "nodeColor",
  42. "nodeSize",
  43. "nodeFlag",
  44. "nodeShape",
  45. "nodeMetadata",
  46. "nodeProperties",
  47. "created_at",
  48. "sketch_id",
  49. ]);
  50. // Properties that are part of nodeMetadata
  51. const METADATA_PROPERTIES = new Set(["created_at"]);
  52. const BATCH_SIZE = 500;
  53. /**
  54. * Main migration function
  55. * @param {import('neo4j-driver').Driver} driver
  56. * @param {import('neo4j-driver').Session} session
  57. * @param {boolean} dryRun
  58. * @returns {Promise<string>} Summary message
  59. */
  60. export async function migrate(driver, session, dryRun) {
  61. // Count nodes needing migration (V1 format: has `type` but no `nodeType`)
  62. const countResult = await session.run(`
  63. MATCH (n)
  64. WHERE n.type IS NOT NULL AND n.nodeType IS NULL
  65. RETURN count(n) AS count
  66. `);
  67. const totalCount = countResult.records[0].get("count").toNumber();
  68. if (totalCount === 0) {
  69. return "No V1 format nodes found - nothing to migrate";
  70. }
  71. console.log(`[INFO] Found ${totalCount} nodes in V1 format to migrate`);
  72. if (dryRun) {
  73. // In dry-run, show sample of what would be migrated
  74. const sampleResult = await session.run(`
  75. MATCH (n)
  76. WHERE n.type IS NOT NULL AND n.nodeType IS NULL
  77. RETURN n, labels(n) AS labels
  78. LIMIT 5
  79. `);
  80. console.log("[DRY-RUN] Sample nodes that would be migrated:");
  81. for (const record of sampleResult.records) {
  82. const node = record.get("n").properties;
  83. const labels = record.get("labels");
  84. console.log(` - [${labels.join(":")}] label="${node.label}", type="${node.type}"`);
  85. }
  86. return `Would migrate ${totalCount} nodes from V1 to V2 format`;
  87. }
  88. // Process in batches
  89. let migratedCount = 0;
  90. let batchNum = 0;
  91. while (migratedCount < totalCount) {
  92. batchNum++;
  93. console.log(
  94. `[INFO] Processing batch ${batchNum} (${migratedCount}/${totalCount} done)`
  95. );
  96. // Fetch a batch of V1 nodes
  97. const batchResult = await session.run(
  98. `
  99. MATCH (n)
  100. WHERE n.type IS NOT NULL AND n.nodeType IS NULL
  101. RETURN elementId(n) AS elementId, n, labels(n) AS labels
  102. LIMIT $limit
  103. `,
  104. { limit: neo4j.int(BATCH_SIZE) }
  105. );
  106. if (batchResult.records.length === 0) {
  107. break;
  108. }
  109. // Process each node in the batch
  110. for (const record of batchResult.records) {
  111. const elementId = record.get("elementId");
  112. const node = record.get("n").properties;
  113. // Build the new properties
  114. const updates = buildV2Properties(node);
  115. // Apply the update
  116. await session.run(
  117. `
  118. MATCH (n)
  119. WHERE elementId(n) = $elementId
  120. SET n += $updates
  121. REMOVE n.label, n.type, n.created_at
  122. `,
  123. { elementId, updates }
  124. );
  125. // Remove old dynamic properties that were moved to nodeProperties
  126. const propsToRemove = Object.keys(node).filter(
  127. (key) =>
  128. !RESERVED_PROPERTIES.has(key) &&
  129. !key.startsWith("nodeProperties.") &&
  130. !key.startsWith("nodeMetadata.")
  131. );
  132. if (propsToRemove.length > 0) {
  133. // Build dynamic REMOVE clause
  134. const removeClause = propsToRemove.map((p) => `n.\`${p}\``).join(", ");
  135. await session.run(
  136. `
  137. MATCH (n)
  138. WHERE elementId(n) = $elementId
  139. REMOVE ${removeClause}
  140. `,
  141. { elementId }
  142. );
  143. }
  144. migratedCount++;
  145. }
  146. }
  147. return `Migrated ${migratedCount} nodes from V1 to V2 format`;
  148. }
  149. /**
  150. * Builds V2 format properties from V1 node
  151. * @param {Record<string, any>} node - V1 node properties
  152. * @returns {Record<string, any>} - V2 format properties to SET
  153. */
  154. function buildV2Properties(node) {
  155. const updates = {};
  156. // Map core fields
  157. updates.nodeLabel = node.label || node.nodeLabel || "";
  158. updates.nodeType = node.type || node.nodeType || "";
  159. // Handle created_at -> nodeMetadata.created_at
  160. if (node.created_at) {
  161. updates["nodeMetadata.created_at"] = node.created_at;
  162. } else if (!node["nodeMetadata.created_at"]) {
  163. // Set current timestamp if no created_at exists
  164. updates["nodeMetadata.created_at"] = new Date().toISOString();
  165. }
  166. // Move non-reserved properties to nodeProperties.*
  167. for (const [key, value] of Object.entries(node)) {
  168. // Skip reserved properties
  169. if (RESERVED_PROPERTIES.has(key)) continue;
  170. // Skip properties already in nodeProperties/nodeMetadata namespace
  171. if (key.startsWith("nodeProperties.") || key.startsWith("nodeMetadata.")) {
  172. continue;
  173. }
  174. // Move to nodeProperties
  175. updates[`nodeProperties.${key}`] = value;
  176. }
  177. return updates;
  178. }