mirror of
https://github.com/reconurge/flowsint.git
synced 2026-04-30 11:19:07 -05:00
feat: neo4j migrations
This commit is contained in:
56
neo4j-migrations/001_indexes.cypher
Normal file
56
neo4j-migrations/001_indexes.cypher
Normal file
@@ -0,0 +1,56 @@
|
||||
// 001_indexes.cypher
|
||||
// Foundation indexes for query performance
|
||||
// All statements are idempotent (IF NOT EXISTS)
|
||||
|
||||
// Index for filtering nodes by sketch_id (most common query pattern)
|
||||
CREATE INDEX idx_sketch_id IF NOT EXISTS FOR (n:domain) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_email IF NOT EXISTS FOR (n:email) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_ip IF NOT EXISTS FOR (n:ip) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_phone IF NOT EXISTS FOR (n:phone) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_username IF NOT EXISTS FOR (n:username) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_organization IF NOT EXISTS FOR (n:organization) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_individual IF NOT EXISTS FOR (n:individual) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_socialaccount IF NOT EXISTS FOR (n:socialaccount) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_asn IF NOT EXISTS FOR (n:asn) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_cidr IF NOT EXISTS FOR (n:cidr) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_cryptowallet IF NOT EXISTS FOR (n:cryptowallet) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_cryptowallettransaction IF NOT EXISTS FOR (n:cryptowallettransaction) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_cryptonft IF NOT EXISTS FOR (n:cryptonft) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_website IF NOT EXISTS FOR (n:website) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_port IF NOT EXISTS FOR (n:port) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_phrase IF NOT EXISTS FOR (n:phrase) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_breach IF NOT EXISTS FOR (n:breach) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_credential IF NOT EXISTS FOR (n:credential) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_device IF NOT EXISTS FOR (n:device) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_document IF NOT EXISTS FOR (n:document) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_file IF NOT EXISTS FOR (n:file) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_malware IF NOT EXISTS FOR (n:malware) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_sslcertificate IF NOT EXISTS FOR (n:sslcertificate) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_location IF NOT EXISTS FOR (n:location) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_affiliation IF NOT EXISTS FOR (n:affiliation) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_alias IF NOT EXISTS FOR (n:alias) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_bankaccount IF NOT EXISTS FOR (n:bankaccount) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_creditcard IF NOT EXISTS FOR (n:creditcard) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_dnsrecord IF NOT EXISTS FOR (n:dnsrecord) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_gravatar IF NOT EXISTS FOR (n:gravatar) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_leak IF NOT EXISTS FOR (n:leak) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_message IF NOT EXISTS FOR (n:message) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_reputationscore IF NOT EXISTS FOR (n:reputationscore) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_riskprofile IF NOT EXISTS FOR (n:riskprofile) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_script IF NOT EXISTS FOR (n:script) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_session IF NOT EXISTS FOR (n:session) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_webtracker IF NOT EXISTS FOR (n:webtracker) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_weapon IF NOT EXISTS FOR (n:weapon) ON (n.sketch_id);
|
||||
CREATE INDEX idx_sketch_id_whois IF NOT EXISTS FOR (n:whois) ON (n.sketch_id);
|
||||
|
||||
// Index for searching by nodeLabel (text search on common types)
|
||||
CREATE INDEX idx_nodeLabel_domain IF NOT EXISTS FOR (n:domain) ON (n.nodeLabel);
|
||||
CREATE INDEX idx_nodeLabel_email IF NOT EXISTS FOR (n:email) ON (n.nodeLabel);
|
||||
CREATE INDEX idx_nodeLabel_ip IF NOT EXISTS FOR (n:ip) ON (n.nodeLabel);
|
||||
CREATE INDEX idx_nodeLabel_phone IF NOT EXISTS FOR (n:phone) ON (n.nodeLabel);
|
||||
CREATE INDEX idx_nodeLabel_username IF NOT EXISTS FOR (n:username) ON (n.nodeLabel);
|
||||
CREATE INDEX idx_nodeLabel_individual IF NOT EXISTS FOR (n:individual) ON (n.nodeLabel);
|
||||
CREATE INDEX idx_nodeLabel_organization IF NOT EXISTS FOR (n:organization) ON (n.nodeLabel);
|
||||
CREATE INDEX idx_nodeLabel_socialaccount IF NOT EXISTS FOR (n:socialaccount) ON (n.nodeLabel);
|
||||
CREATE INDEX idx_nodeLabel_website IF NOT EXISTS FOR (n:website) ON (n.nodeLabel);
|
||||
CREATE INDEX idx_nodeLabel_cryptowallet IF NOT EXISTS FOR (n:cryptowallet) ON (n.nodeLabel);
|
||||
204
neo4j-migrations/003_migrate_v1_format.js
Normal file
204
neo4j-migrations/003_migrate_v1_format.js
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 003_migrate_v1_format.js
|
||||
*
|
||||
* Migrates nodes from V1 format to V2 format.
|
||||
*
|
||||
* V1 format (old):
|
||||
* label: "example.com"
|
||||
* type: "domain"
|
||||
* created_at: "2026-01-23T18:28:46.048223+00:00"
|
||||
* domain: "example.com"
|
||||
* root: true
|
||||
* sketch_id: "..."
|
||||
* x, y: coordinates
|
||||
*
|
||||
* V2 format (new):
|
||||
* nodeLabel: "example.com"
|
||||
* nodeType: "domain"
|
||||
* nodeMetadata.created_at: "2026-01-23T18:28:46.048223+00:00"
|
||||
* nodeProperties.domain: "example.com"
|
||||
* nodeProperties.root: true
|
||||
* sketch_id: "..."
|
||||
* x, y: coordinates
|
||||
*
|
||||
* This migration is IDEMPOTENT:
|
||||
* - Only processes nodes that have V1 format (have `label` or `type` but NOT `nodeLabel`)
|
||||
* - Safe to run multiple times
|
||||
* - Processes in batches to handle large datasets
|
||||
*/
|
||||
|
||||
// Reserved properties that should NOT be moved to nodeProperties
|
||||
const RESERVED_PROPERTIES = new Set([
|
||||
"id",
|
||||
"x",
|
||||
"y",
|
||||
"nodeLabel",
|
||||
"label",
|
||||
"nodeType",
|
||||
"type",
|
||||
"nodeImage",
|
||||
"nodeIcon",
|
||||
"nodeColor",
|
||||
"nodeSize",
|
||||
"nodeFlag",
|
||||
"nodeShape",
|
||||
"nodeMetadata",
|
||||
"nodeProperties",
|
||||
"created_at",
|
||||
"sketch_id",
|
||||
]);
|
||||
|
||||
// Properties that are part of nodeMetadata
|
||||
const METADATA_PROPERTIES = new Set(["created_at"]);
|
||||
|
||||
const BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* Main migration function
|
||||
* @param {import('neo4j-driver').Driver} driver
|
||||
* @param {import('neo4j-driver').Session} session
|
||||
* @param {boolean} dryRun
|
||||
* @returns {Promise<string>} Summary message
|
||||
*/
|
||||
export async function migrate(driver, session, dryRun) {
|
||||
// Count nodes needing migration (V1 format: has `type` but no `nodeType`)
|
||||
const countResult = await session.run(`
|
||||
MATCH (n)
|
||||
WHERE n.type IS NOT NULL AND n.nodeType IS NULL
|
||||
RETURN count(n) AS count
|
||||
`);
|
||||
const totalCount = countResult.records[0].get("count").toNumber();
|
||||
|
||||
if (totalCount === 0) {
|
||||
return "No V1 format nodes found - nothing to migrate";
|
||||
}
|
||||
|
||||
console.log(`[INFO] Found ${totalCount} nodes in V1 format to migrate`);
|
||||
|
||||
if (dryRun) {
|
||||
// In dry-run, show sample of what would be migrated
|
||||
const sampleResult = await session.run(`
|
||||
MATCH (n)
|
||||
WHERE n.type IS NOT NULL AND n.nodeType IS NULL
|
||||
RETURN n, labels(n) AS labels
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
console.log("[DRY-RUN] Sample nodes that would be migrated:");
|
||||
for (const record of sampleResult.records) {
|
||||
const node = record.get("n").properties;
|
||||
const labels = record.get("labels");
|
||||
console.log(` - [${labels.join(":")}] label="${node.label}", type="${node.type}"`);
|
||||
}
|
||||
|
||||
return `Would migrate ${totalCount} nodes from V1 to V2 format`;
|
||||
}
|
||||
|
||||
// Process in batches
|
||||
let migratedCount = 0;
|
||||
let batchNum = 0;
|
||||
|
||||
while (migratedCount < totalCount) {
|
||||
batchNum++;
|
||||
console.log(
|
||||
`[INFO] Processing batch ${batchNum} (${migratedCount}/${totalCount} done)`
|
||||
);
|
||||
|
||||
// Fetch a batch of V1 nodes
|
||||
const batchResult = await session.run(
|
||||
`
|
||||
MATCH (n)
|
||||
WHERE n.type IS NOT NULL AND n.nodeType IS NULL
|
||||
RETURN elementId(n) AS elementId, n, labels(n) AS labels
|
||||
LIMIT $limit
|
||||
`,
|
||||
{ limit: BATCH_SIZE }
|
||||
);
|
||||
|
||||
if (batchResult.records.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Process each node in the batch
|
||||
for (const record of batchResult.records) {
|
||||
const elementId = record.get("elementId");
|
||||
const node = record.get("n").properties;
|
||||
|
||||
// Build the new properties
|
||||
const updates = buildV2Properties(node);
|
||||
|
||||
// Apply the update
|
||||
await session.run(
|
||||
`
|
||||
MATCH (n)
|
||||
WHERE elementId(n) = $elementId
|
||||
SET n += $updates
|
||||
REMOVE n.label, n.type, n.created_at
|
||||
`,
|
||||
{ elementId, updates }
|
||||
);
|
||||
|
||||
// Remove old dynamic properties that were moved to nodeProperties
|
||||
const propsToRemove = Object.keys(node).filter(
|
||||
(key) =>
|
||||
!RESERVED_PROPERTIES.has(key) &&
|
||||
!key.startsWith("nodeProperties.") &&
|
||||
!key.startsWith("nodeMetadata.")
|
||||
);
|
||||
|
||||
if (propsToRemove.length > 0) {
|
||||
// Build dynamic REMOVE clause
|
||||
const removeClause = propsToRemove.map((p) => `n.\`${p}\``).join(", ");
|
||||
await session.run(
|
||||
`
|
||||
MATCH (n)
|
||||
WHERE elementId(n) = $elementId
|
||||
REMOVE ${removeClause}
|
||||
`,
|
||||
{ elementId }
|
||||
);
|
||||
}
|
||||
|
||||
migratedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return `Migrated ${migratedCount} nodes from V1 to V2 format`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds V2 format properties from V1 node
|
||||
* @param {Record<string, any>} node - V1 node properties
|
||||
* @returns {Record<string, any>} - V2 format properties to SET
|
||||
*/
|
||||
function buildV2Properties(node) {
|
||||
const updates = {};
|
||||
|
||||
// Map core fields
|
||||
updates.nodeLabel = node.label || node.nodeLabel || "";
|
||||
updates.nodeType = node.type || node.nodeType || "";
|
||||
|
||||
// Handle created_at -> nodeMetadata.created_at
|
||||
if (node.created_at) {
|
||||
updates["nodeMetadata.created_at"] = node.created_at;
|
||||
} else if (!node["nodeMetadata.created_at"]) {
|
||||
// Set current timestamp if no created_at exists
|
||||
updates["nodeMetadata.created_at"] = new Date().toISOString();
|
||||
}
|
||||
|
||||
// Move non-reserved properties to nodeProperties.*
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
// Skip reserved properties
|
||||
if (RESERVED_PROPERTIES.has(key)) continue;
|
||||
|
||||
// Skip properties already in nodeProperties/nodeMetadata namespace
|
||||
if (key.startsWith("nodeProperties.") || key.startsWith("nodeMetadata.")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move to nodeProperties
|
||||
updates[`nodeProperties.${key}`] = value;
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
@@ -13,13 +13,19 @@
|
||||
"release:major": "standard-version --release-as major",
|
||||
"release:patch": "standard-version --release-as patch",
|
||||
"release:first": "standard-version --first-release",
|
||||
"sync-version": "node scripts/sync-versions.js"
|
||||
"sync-version": "node scripts/sync-versions.js",
|
||||
"migrate": "node scripts/migrate.js",
|
||||
"migrate:dry-run": "node scripts/migrate.js --dry-run"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.7",
|
||||
"neo4j-driver": "^5.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"husky": "^9.1.7",
|
||||
"@commitlint/cli": "^20.1.0",
|
||||
|
||||
242
scripts/migrate.js
Normal file
242
scripts/migrate.js
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Neo4j Migration Runner
|
||||
*
|
||||
* Runs migrations from neo4j-migrations/ directory in order.
|
||||
* Tracks applied migrations in (:_Migration) nodes to prevent re-running.
|
||||
* All migrations must be idempotent for safety.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/migrate.js [--dry-run]
|
||||
*
|
||||
* Environment variables (from .env or shell):
|
||||
* NEO4J_URI_BOLT - Bolt URI (default: bolt://localhost:7687)
|
||||
* NEO4J_USERNAME - Username (default: neo4j)
|
||||
* NEO4J_PASSWORD - Password (required)
|
||||
*/
|
||||
|
||||
import "dotenv/config";
|
||||
import neo4j from "neo4j-driver";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const MIGRATIONS_DIR = path.join(__dirname, "..", "neo4j-migrations");
|
||||
|
||||
const config = {
|
||||
uri: "bolt://localhost:7687",
|
||||
user: process.env.NEO4J_USERNAME || "neo4j",
|
||||
password: process.env.NEO4J_PASSWORD,
|
||||
};
|
||||
|
||||
const isDryRun = process.argv.includes("--dry-run");
|
||||
|
||||
/**
|
||||
* Logger with consistent formatting
|
||||
*/
|
||||
const log = {
|
||||
info: (msg) => console.log(`[INFO] ${msg}`),
|
||||
warn: (msg) => console.log(`[WARN] ${msg}`),
|
||||
error: (msg) => console.error(`[ERROR] ${msg}`),
|
||||
success: (msg) => console.log(`[OK] ${msg}`),
|
||||
dry: (msg) => console.log(`[DRY-RUN] ${msg}`),
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensures the _Migration tracking infrastructure exists
|
||||
*/
|
||||
async function ensureMigrationInfrastructure(session) {
|
||||
await session.run(`
|
||||
CREATE CONSTRAINT migration_name_unique IF NOT EXISTS
|
||||
FOR (m:_Migration) REQUIRE m.name IS UNIQUE
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets list of already applied migrations
|
||||
*/
|
||||
async function getAppliedMigrations(session) {
|
||||
const result = await session.run(`
|
||||
MATCH (m:_Migration)
|
||||
RETURN m.name AS name
|
||||
ORDER BY m.name
|
||||
`);
|
||||
return new Set(result.records.map((r) => r.get("name")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a migration as applied
|
||||
*/
|
||||
async function recordMigration(session, name) {
|
||||
await session.run(
|
||||
`
|
||||
MERGE (m:_Migration {name: $name})
|
||||
ON CREATE SET m.applied_at = datetime()
|
||||
ON MATCH SET m.last_run_at = datetime()
|
||||
`,
|
||||
{ name },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all migration files sorted by name
|
||||
*/
|
||||
async function getMigrationFiles() {
|
||||
const files = await fs.readdir(MIGRATIONS_DIR);
|
||||
return files
|
||||
.filter((f) => f.endsWith(".cypher") || f.endsWith(".js"))
|
||||
.filter((f) => !f.startsWith("_")) // Skip files starting with _
|
||||
.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a .cypher migration file
|
||||
* Splits on semicolons and runs each statement
|
||||
*/
|
||||
async function runCypherMigration(session, filePath, dryRun) {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
|
||||
// Split by semicolons, filter empty statements and comments-only blocks
|
||||
const statements = content
|
||||
.split(";")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => {
|
||||
// Remove comment-only statements
|
||||
const withoutComments = s
|
||||
.split("\n")
|
||||
.filter((line) => !line.trim().startsWith("//"))
|
||||
.join("\n")
|
||||
.trim();
|
||||
return withoutComments.length > 0;
|
||||
});
|
||||
|
||||
for (const statement of statements) {
|
||||
if (dryRun) {
|
||||
log.dry(`Would execute: ${statement.substring(0, 80)}...`);
|
||||
} else {
|
||||
await session.run(statement);
|
||||
}
|
||||
}
|
||||
|
||||
return statements.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a .js migration file
|
||||
* The file must export a `migrate(driver, session, dryRun)` function
|
||||
*/
|
||||
async function runJsMigration(driver, session, filePath, dryRun) {
|
||||
const module = await import(
|
||||
fileURLToPath(new URL(filePath, import.meta.url))
|
||||
);
|
||||
|
||||
if (typeof module.migrate !== "function") {
|
||||
throw new Error(`Migration ${filePath} must export a 'migrate' function`);
|
||||
}
|
||||
|
||||
return await module.migrate(driver, session, dryRun);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main migration runner
|
||||
*/
|
||||
async function main() {
|
||||
if (!config.password) {
|
||||
log.error("NEO4J_PASSWORD environment variable is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
log.info("Running in dry-run mode - no changes will be made");
|
||||
}
|
||||
|
||||
log.info(`Connecting to Neo4j at ${config.uri}`);
|
||||
|
||||
const driver = neo4j.driver(
|
||||
config.uri,
|
||||
neo4j.auth.basic(config.user, config.password),
|
||||
);
|
||||
|
||||
try {
|
||||
// Verify connectivity
|
||||
await driver.verifyConnectivity();
|
||||
log.success("Connected to Neo4j");
|
||||
|
||||
const session = driver.session();
|
||||
|
||||
try {
|
||||
// Setup migration tracking
|
||||
if (!isDryRun) {
|
||||
await ensureMigrationInfrastructure(session);
|
||||
}
|
||||
|
||||
// Get applied migrations
|
||||
const applied = isDryRun
|
||||
? new Set()
|
||||
: await getAppliedMigrations(session);
|
||||
if (applied.size > 0) {
|
||||
log.info(`Found ${applied.size} previously applied migrations`);
|
||||
}
|
||||
|
||||
// Get all migration files
|
||||
const files = await getMigrationFiles();
|
||||
log.info(`Found ${files.length} migration files`);
|
||||
|
||||
let appliedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const migrationName = file.replace(/\.(cypher|js)$/, "");
|
||||
const filePath = path.join(MIGRATIONS_DIR, file);
|
||||
|
||||
if (applied.has(migrationName)) {
|
||||
log.info(`Skipping ${file} (already applied)`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info(`Running migration: ${file}`);
|
||||
|
||||
try {
|
||||
if (file.endsWith(".cypher")) {
|
||||
const count = await runCypherMigration(session, filePath, isDryRun);
|
||||
log.success(`${file}: executed ${count} statements`);
|
||||
} else if (file.endsWith(".js")) {
|
||||
const result = await runJsMigration(
|
||||
driver,
|
||||
session,
|
||||
filePath,
|
||||
isDryRun,
|
||||
);
|
||||
log.success(`${file}: ${result || "completed"}`);
|
||||
}
|
||||
|
||||
// Record migration as applied
|
||||
if (!isDryRun) {
|
||||
await recordMigration(session, migrationName);
|
||||
}
|
||||
|
||||
appliedCount++;
|
||||
} catch (err) {
|
||||
log.error(`Migration ${file} failed: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("---");
|
||||
log.success(
|
||||
`Migration complete: ${appliedCount} applied, ${skippedCount} skipped`,
|
||||
);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
} catch (err) {
|
||||
log.error(`Migration failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await driver.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
42
yarn.lock
42
yarn.lock
@@ -2972,6 +2972,14 @@ buffer@^5.5.0:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
buffer@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
|
||||
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
cachedir@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
|
||||
@@ -4016,6 +4024,11 @@ dot-prop@^5.1.0:
|
||||
dependencies:
|
||||
is-obj "^2.0.0"
|
||||
|
||||
dotenv@^16.4.7:
|
||||
version "16.6.1"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020"
|
||||
integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==
|
||||
|
||||
dotgitignore@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b"
|
||||
@@ -5113,7 +5126,7 @@ iconv-lite@^0.4.24:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
ieee754@^1.1.13:
|
||||
ieee754@^1.1.13, ieee754@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
@@ -6578,6 +6591,29 @@ neo-async@^2.6.2:
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
|
||||
neo4j-driver-bolt-connection@5.28.3:
|
||||
version "5.28.3"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-5.28.3.tgz#62c5a3a2b6a018fdff422ba2fa379f220d11dd7a"
|
||||
integrity sha512-wqHBYcU0FVRDmdsoZ+Fk0S/InYmu9/4BT6fPYh45Jimg/J7vQBUcdkiHGU7nop7HRb1ZgJmL305mJb6g5Bv35Q==
|
||||
dependencies:
|
||||
buffer "^6.0.3"
|
||||
neo4j-driver-core "5.28.3"
|
||||
string_decoder "^1.3.0"
|
||||
|
||||
neo4j-driver-core@5.28.3:
|
||||
version "5.28.3"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-5.28.3.tgz#ae1b1ab902db978396a3f4102c63b95f5b053b6e"
|
||||
integrity sha512-Jk+hAmjFmO5YzVH/U7FyKXigot9zmIfLz6SZQy0xfr4zfTE/S8fOYFOGqKQTHBE86HHOWH2RbTslbxIb+XtU2g==
|
||||
|
||||
neo4j-driver@^5.27.0:
|
||||
version "5.28.3"
|
||||
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-5.28.3.tgz#5f7a273a2e157d2c3c19abc1c5189c9c79abe44b"
|
||||
integrity sha512-k7c0wEh3HoONv1v5AyLp9/BDAbYHJhz2TZvzWstSEU3g3suQcXmKEaYBfrK2UMzxcy3bCT0DrnfRbzsOW5G/Ag==
|
||||
dependencies:
|
||||
neo4j-driver-bolt-connection "5.28.3"
|
||||
neo4j-driver-core "5.28.3"
|
||||
rxjs "^7.8.2"
|
||||
|
||||
next-themes@^0.4.6:
|
||||
version "0.4.6"
|
||||
resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz"
|
||||
@@ -7706,7 +7742,7 @@ rw@1:
|
||||
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
|
||||
integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
|
||||
|
||||
rxjs@^7.5.5:
|
||||
rxjs@^7.5.5, rxjs@^7.8.2:
|
||||
version "7.8.2"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b"
|
||||
integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==
|
||||
@@ -8065,7 +8101,7 @@ string.prototype.trimstart@^1.0.8:
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
string_decoder@^1.1.1, string_decoder@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
|
||||
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
|
||||
|
||||
Reference in New Issue
Block a user