diff --git a/backend/src/db/migrate/store.js b/backend/src/db/migrate/store.js index 97b057dac..81df47708 100644 --- a/backend/src/db/migrate/store.js +++ b/backend/src/db/migrate/store.js @@ -35,7 +35,7 @@ class Store { const session = driver.session() const readTxResultPromise = session.readTransaction(async txc => { const result = await txc.run( - 'MATCH (migration:Migration) RETURN migration {.*} ORDER BY migration.timestamp DESC', + 'MATCH (migration:Migration) RETURN migration {.*} ORDER BY migration.createdAt DESC', ) return result.records.map(r => r.get('migration')) }) @@ -64,12 +64,19 @@ class Store { const { migrations } = set const writeTxResultPromise = session.writeTransaction(txc => { return Promise.all( - migrations.map(migration => { - const { title, description, timestamp } = migration - const properties = { title, description, timestamp } - return txc.run('CREATE (migration:Migration) SET migration += $properties', { - properties, - }) + migrations.map(async migration => { + const { title, description } = migration + const properties = { title, description } + const migrationResult = await txc.run( + ` + MERGE (migration:Migration { title: $properties.title }) + ON CREATE SET migration += $properties, + migration.createdAt = toString(datetime()) + RETURN migration + `, + { properties }, + ) + return migrationResult }), ) }) diff --git a/backend/src/db/migrations/1579387929111-merge_duplicate_location_nodes.js b/backend/src/db/migrations/1579387929111-merge_duplicate_location_nodes.js new file mode 100644 index 000000000..390bfd935 --- /dev/null +++ b/backend/src/db/migrations/1579387929111-merge_duplicate_location_nodes.js @@ -0,0 +1,77 @@ +import { throwError, concat } from 'rxjs' +import { flatMap, mergeMap, map, catchError } from 'rxjs/operators' +import { getDriver } from '../neo4j' + +export const description = ` + This migration merges duplicate :Location nodes. It became + necessary after we realized that we had not set up constraints for Location.id in production. +` +export function up(next) { + const driver = getDriver() + const rxSession = driver.rxSession() + rxSession + .beginTransaction() + .pipe( + flatMap(transaction => + concat( + transaction + .run( + ` + MATCH (location:Location) + RETURN location {.id} + `, + ) + .records() + .pipe( + map(record => { + const { id: locationIds } = record.get('location') + return { locationIds } + }), + mergeMap(({ locationIds }) => { + return transaction + .run( + ` + MATCH(location:Location {id: $locationIds}), (location2:Location {id: $locationIds}) + WHERE location.id = location2.id AND id(location) < id(location2) + CALL apoc.refactor.mergeNodes([location, location2], { properties: 'combine', mergeRels: true }) YIELD node as updatedLocation + RETURN location {.*},updatedLocation {.*} + `, + { locationIds }, + ) + .records() + .pipe( + map(record => ({ + location: record.get('location'), + updatedLocation: record.get('updatedLocation'), + })), + ) + }), + ), + transaction.commit(), + ).pipe(catchError(error => transaction.rollback().pipe(throwError(error)))), + ), + ) + .subscribe({ + next: ({ updatedLocation, location }) => + // eslint-disable-next-line no-console + console.log(` + Merged: + ============================= + locationId: ${location.id} + updatedLocation: ${location.id} => ${updatedLocation.id} + ============================= + `), + complete: () => { + // eslint-disable-next-line no-console + console.log('Merging of duplicate locations completed') + next() + }, + error: error => { + next(new Error(error), null) + }, + }) +} + +export function down(next) { + next(new Error('Irreversible migration')) +}