Search code examples
mongodbmongooseapollo-serveraudit-trail

How can I retrieve an id from MongoDB create operation during a transaction?


I am trying to create an audit trail using Apollo Server and Mongoose. When a user initially registers, I create a document in the users collection and a document in the history collection for each piece of data they provided (username, password, email, etc) . For each history collection document, I include the id for the user document to create a relationship. Works perfectly.

However, when I add a transaction in (see below), the userId for the user document comes back as undefined, so I cannot add it to the history entry documents. I am assuming that the id for a document does not get created until the entire transaction has been completed?

Any ideas?

Mutation: {
  register: async (_, { data }) => {

    // Start a mongo session & transaction
    const session = await mongoose.startSession()
    session.startTransaction()

    try {

      // Hash password and create user
      const hashedPassword = await bcrypt.hash(data.password, 12)
      const user = await User.create(
        [{ ...data, password: hashedPassword }],
        { session }
      )
      
      // Add history entries
      HistoryEntry.create([
      {
        user: user.id,
        action: 'registered'
      },
      {
        user: user.id,
        action: 'set',
        object: 'profile',
        instance: user.id,
        property: 'firstName',
        value: firstName
      },
      {
        user: user.id,
        action: 'set',
        object: 'profile',
        instance: user.id,
        property: 'lastName',
        value: lastName
      },
      {
        user: user.id,
        action: 'set',
        object: 'profile',
        instance: user.id,
        property: 'password'
      }
    ])

    if (loginType === 'email') {
      HistoryEntry.create({
        user: user.id,
        action: 'set',
        object: 'profile',
        instance: user.id,
        property: 'email',
        value: login
      })
    }

    if (loginType === 'mobile') {
      HistoryEntry.create({
        user: user.id,
        action: 'set',
        object: 'profile',
        instance: user.id,
        property: 'mobile',
        value: login
      })
    }

    // commit the changes if everything was successful
    await session.commitTransaction()
      return {
        ok: true,
        user
      }
    } catch (err) {
      // if anything fails above, rollback the changes in the transaction
      await session.abortTransaction()
      return formatErrors(err)
    } finally {
      // end the session
      session.endSession()
    }
  }
}

Solution

  • If you think about it, how can you add a HistoryEntry if you haven't added User yet? It's not a 'history' as you are currently doing it. I believe you got two options here - set _id on User manually new Schema({ _id: { type: Schema.ObjectId, auto: true }}) and then generate it within the transaction: var userId = ObjectId(); and use for both User and History Entries.

    And the second option, more semantically correct in this context, I believe - you should attach to post-save hook:

    schema.post('save', function(doc) {
      console.log('%s has been saved', doc._id);
    });
    

    So, whenever an User is created, a post-save hook is fired to update History.