From 0cfefe05ac908e9c89474790075ffc6086626158 Mon Sep 17 00:00:00 2001
From: Stefano Ricci <1219739+SteRiccio@users.noreply.github.com>
Date: Thu, 2 Nov 2023 16:21:21 +0100
Subject: [PATCH] Data import from mobile: check invalid parent uuid (deleted
 nodes) (#3127)

* fixing data import issues (WIP)

* check invalid parent uuid

---------

Co-authored-by: Stefano Ricci <SteRiccio@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
 .../jobs/recordsImportJob.js                  | 32 +++++++++++++------
 .../modules/user/repository/userRepository.js |  2 +-
 2 files changed, 23 insertions(+), 11 deletions(-)

diff --git a/server/modules/mobile/service/arenaMobileDataImport/jobs/recordsImportJob.js b/server/modules/mobile/service/arenaMobileDataImport/jobs/recordsImportJob.js
index e6cf24df09..24748bfa67 100644
--- a/server/modules/mobile/service/arenaMobileDataImport/jobs/recordsImportJob.js
+++ b/server/modules/mobile/service/arenaMobileDataImport/jobs/recordsImportJob.js
@@ -6,6 +6,7 @@ import * as Survey from '@core/survey/survey'
 import * as NodeDef from '@core/survey/nodeDef'
 import * as Record from '@core/record/record'
 import * as Node from '@core/record/node'
+import * as User from '@core/user/user'
 import * as ObjectUtils from '@core/objectUtils'
 import * as PromiseUtils from '@core/promiseUtils'
 
@@ -13,6 +14,7 @@ import * as ArenaSurveyFileZip from '@server/modules/arenaImport/service/arenaIm
 import DataImportBaseJob from '@server/modules/dataImport/service/DataImportJob/DataImportBaseJob'
 import * as RecordManager from '@server/modules/record/manager/recordManager'
 import * as SurveyService from '@server/modules/survey/service/surveyService'
+import * as UserService from '@server/modules/user/service/userService'
 
 export default class RecordsImportJob extends DataImportBaseJob {
   constructor(params) {
@@ -38,8 +40,8 @@ export default class RecordsImportJob extends DataImportBaseJob {
       const recordUuid = Record.getUuid(recordSummary)
 
       const record = await ArenaSurveyFileZip.getRecord(arenaSurveyFileZip, recordUuid)
-      this.cleanupRecord(record)
       this.currentRecord = record
+      await this.cleanupCurrentRecord()
 
       await this.insertOrSkipRecord()
 
@@ -47,32 +49,42 @@ export default class RecordsImportJob extends DataImportBaseJob {
     })
   }
 
-  cleanupRecord(record) {
-    const { survey } = this
+  async cleanupCurrentRecord() {
+    const { context, currentRecord: record, user, tx } = this
+    const { survey } = context
+
+    // check owner uuid: if user not defined, use the job user as owner
+    const ownerUuidSource = Record.getOwnerUuid(record)
+    const ownerSource = await UserService.fetchUserByUuid(ownerUuidSource, tx)
+    record[Record.keys.ownerUuid] = ownerSource ? ownerUuidSource : User.getUuid(user)
+
+    // remove invalid nodes and build index from scratch
     delete record['_nodesIndex']
     const nodes = Record.getNodes(record)
 
-    // remove invalid nodes
     Object.entries(nodes).forEach(([nodeUuid, node]) => {
       const nodeDef = Survey.getNodeDefByUuid(Node.getNodeDefUuid(node))(survey)
-      const missingParentUuid = !Node.getParentUuid(node) && !NodeDef.isRoot(nodeDef)
+      const parentUuid = Node.getParentUuid(node)
+      const missingParentUuid = (!parentUuid && !NodeDef.isRoot(nodeDef)) || (parentUuid && !nodes[parentUuid])
       const emptyMultipleAttribute = NodeDef.isMultiple(nodeDef) && Node.isValueBlank(node)
 
       if (missingParentUuid || emptyMultipleAttribute) {
         const messagePrefix = `node with uuid ${Node.getUuid(node)}`
-        const messageContent = missingParentUuid ? `has missing parent_uuid` : `is multiple and has an empty value`
+        const messageContent = missingParentUuid
+          ? `has missing or invalid parent_uuid`
+          : `is multiple and has an empty value`
         const messageSuffix = `: skipping it`
         this.logWarn(`${messagePrefix} ${messageContent} ${messageSuffix}`)
         delete nodes[nodeUuid]
       }
     })
     // assoc nodes and build index from scratch
-    return Record.assocNodes({ nodes, sideEffect: true })(record)
+    this.currentRecord = Record.assocNodes({ nodes, sideEffect: true })(record)
   }
 
   async insertOrSkipRecord() {
-    const { context, currentRecord: record, user, tx } = this
-    const { survey, surveyId, conflictResolutionStrategy } = context
+    const { context, currentRecord: record, tx } = this
+    const { surveyId, conflictResolutionStrategy } = context
 
     const recordUuid = Record.getUuid(record)
 
@@ -90,7 +102,7 @@ export default class RecordsImportJob extends DataImportBaseJob {
         await this.updateExistingRecord()
       }
     } else {
-      await this.insertNewRecord(recordUuid, user, surveyId, record, tx, survey)
+      await this.insertNewRecord()
     }
   }
 
diff --git a/server/modules/user/repository/userRepository.js b/server/modules/user/repository/userRepository.js
index b5150d9ccf..da6b882464 100644
--- a/server/modules/user/repository/userRepository.js
+++ b/server/modules/user/repository/userRepository.js
@@ -210,7 +210,7 @@ export const fetchUserByUuidWithPassword = async (uuid, client = db) =>
   )
 
 export const fetchUserByUuid = async (uuid, client = db) =>
-  client.one(
+  client.oneOrNone(
     `
     SELECT ${columnsCommaSeparated}, u.profile_picture IS NOT NULL as has_profile_picture
     FROM "user" u