Răsfoiți Sursa

close loop on account deletion; remove all herds and tasks too

also some refactoring around transactions: models should not self-manage session state but defer to a session passed in, if any
Aneurin Barker Snook 1 an în urmă
părinte
comite
d1984f623b
6 a modificat fișierele cu 56 adăugiri și 84 ștergeri
  1. 17 4
      src/account/api.ts
  2. 23 0
      src/account/model.ts
  3. 4 30
      src/herd/model.ts
  4. 1 0
      src/main.ts
  5. 5 50
      src/task/model.ts
  6. 6 0
      src/types.ts

+ 17 - 4
src/account/api.ts

@@ -1,6 +1,5 @@
 import type { AuthRequestHandler } from '../auth'
 import type { Context } from '../types'
-import { ObjectId } from 'mongodb'
 import type { RequestHandler } from 'express'
 import type { WithId } from 'mongodb'
 import type { Account, AccountCreate, AccountUpdate } from './types'
@@ -48,6 +47,12 @@ export function createAccount({ model }: Context): RequestHandler {
 export function deleteAccount({ model }: Context): AuthRequestHandler {
   interface ResponseData {
     account: WithId<Account>
+    herds: {
+      deletedCount: number
+    }
+    tasks: {
+      deletedCount: number
+    }
   }
 
   return async function (req, res, next) {
@@ -60,11 +65,19 @@ export function deleteAccount({ model }: Context): AuthRequestHandler {
 
     try {
       // Delete account
-      /** @todo delete related data */
-      const account = await model.account.collection.findOneAndDelete({ _id: new ObjectId(id) })
+      const { account, deletedHerds, deletedTasks } = await model.account.delete(id)
       if (!account) return http.notFound(res, next)
 
-      const output: ResponseData = { account }
+      // Send output
+      const output: ResponseData = {
+        account,
+        herds: {
+          deletedCount: deletedHerds,
+        },
+        tasks: {
+          deletedCount: deletedTasks,
+        },
+      }
       res.send(output)
       next()
     } catch (err) {

+ 23 - 0
src/account/model.ts

@@ -1,3 +1,4 @@
+import type { ClientSession } from 'mongodb'
 import type { Context } from '../types'
 import { ObjectId } from 'mongodb'
 import crypto from 'crypto'
@@ -27,6 +28,27 @@ async function createAccountModel(ctx: Context) {
     return await collection.findOne({ _id: result.insertedId })
   }
 
+  /**
+   * Delete an account.
+   * This function also deletes any related data, including herds and tasks.
+   */
+  async function _delete(id: ObjectId | string, session?: ClientSession) {
+    let deletedHerds = 0
+    let deletedTasks = 0
+
+    // Delete herds
+    const herds = ctx.ctx().model.herd.collection.find({ _account: new ObjectId(id) }, { projection: { _id: 1 }, session })
+    for await (const herd of herds) {
+      const result = await ctx.ctx().model.herd.delete(herd._id)
+      deletedHerds++
+      deletedTasks += result.deletedCount
+    }
+
+    const account = await collection.findOneAndDelete({ _id: new ObjectId(id) }, { session })
+
+    return { account, deletedHerds, deletedTasks }
+  }
+
   /** Generate a salt for use in password hashing. */
   function generateSalt() {
     return crypto.randomBytes(32).toString('hex')
@@ -72,6 +94,7 @@ async function createAccountModel(ctx: Context) {
   return {
     collection,
     create,
+    delete: _delete,
     generateSalt,
     hashPassword,
     init,

+ 4 - 30
src/herd/model.ts

@@ -1,6 +1,6 @@
+import type { ClientSession } from 'mongodb'
 import type { Context } from '../types'
 import { ObjectId } from 'mongodb'
-import type { Task } from '../task/types'
 import type { Herd, HerdCreate, HerdUpdate } from './types'
 
 /** Model for accessing and managing herds. */
@@ -9,7 +9,6 @@ export type HerdModel = Awaited<ReturnType<typeof createHerdModel>>
 /** Create a herd model. */
 async function createHerdModel(ctx: Context) {
   const collection = ctx.db.collection<Herd>('herd')
-  const taskCollection = ctx.db.collection<Task>('task')
 
   /** Create a herd. */
   async function create(input: HerdCreate) {
@@ -40,41 +39,16 @@ async function createHerdModel(ctx: Context) {
    * Delete a herd.
    * This function removes all tasks associated with the herd and returns the number of tasks deleted.
    */
-  async function _delete(id: ObjectId) {
-    if (ctx.config.mongo.useTransactions) return _deleteTx(id)
-
+  async function _delete(id: ObjectId, session?: ClientSession) {
     // Delete tasks
-    const { deletedCount } = await taskCollection.deleteMany({ _herd: id })
+    const { deletedCount } = await ctx.ctx().model.task.collection.deleteMany({ _herd: id }, { session })
 
     // Delete herd
-    const herd = await collection.findOneAndDelete({ _id: id })
+    const herd = await collection.findOneAndDelete({ _id: id }, { session })
 
     return { herd, deletedCount }
   }
 
-  /** Delete a herd using a transaction. */
-  async function _deleteTx(id: ObjectId) {
-    const session = ctx.mongo.startSession()
-    try {
-      session.startTransaction()
-
-      // Delete tasks
-      const { deletedCount } = await taskCollection.deleteMany({ _herd: id }, { session })
-
-      // Delete herd
-      const herd = await collection.findOneAndDelete({ _id: id })
-
-      // Commit and return
-      await session.commitTransaction()
-      return { herd, deletedCount }
-    } catch (err) {
-      await session.abortTransaction()
-      throw err
-    } finally {
-      await session.endSession()
-    }
-  }
-
   /**
    * Update a herd.
    */

+ 1 - 0
src/main.ts

@@ -10,6 +10,7 @@ import type { Config, Context } from './types'
 async function main(config: Config): Promise<void> {
   // Create context
   const ctx = <Context>{ config }
+  ctx.ctx = () => ctx
 
   // Initialize logger
   const log = createLogger(ctx)

+ 5 - 50
src/task/model.ts

@@ -1,3 +1,4 @@
+import type { ClientSession } from 'mongodb'
 import type { Context } from '../types'
 import { ObjectId } from 'mongodb'
 import type { Task, TaskCreate, TaskUpdate } from './types'
@@ -44,14 +45,12 @@ async function createTaskModel(ctx: Context) {
    * Move a task.
    * This function updates the position of all subsequent tasks and returns the total number of tasks affected.
    */
-  async function move(id: ObjectId | string, position: number) {
-    if (ctx.config.mongo.useTransactions) return moveTx(id, position)
-
+  async function move(id: ObjectId | string, position: number, session?: ClientSession) {
     // Update specified task
     const task = await collection.findOneAndUpdate(
       { _id: new ObjectId(id) },
       { $set: { position } },
-      { returnDocument: 'after' },
+      { returnDocument: 'after', session },
     )
     // Not expected to happen, but type-safe
     if (!task) throw new Error('failed to get updated task')
@@ -61,8 +60,7 @@ async function createTaskModel(ctx: Context) {
       _herd: task._herd,
       _id: { $ne: new ObjectId(id) },
       position: { $gte: position },
-
-    }).sort('position', 1)
+    }, { session }).sort('position', 1)
 
     // Update subsequent tasks
     let affectedCount = 1
@@ -70,6 +68,7 @@ async function createTaskModel(ctx: Context) {
       await collection.updateOne(
         { _id: task._id },
         { $set: { position: position + affectedCount } },
+        { session },
       )
       affectedCount++
     }
@@ -77,50 +76,6 @@ async function createTaskModel(ctx: Context) {
     return { task, affectedCount }
   }
 
-  /** Move a task using a transaction. */
-  async function moveTx(id: ObjectId | string, position: number) {
-    const session = ctx.mongo.startSession()
-    try {
-      session.startTransaction()
-
-      // Update specified task
-      const task = await collection.findOneAndUpdate(
-        { _id: new ObjectId(id) },
-        { $set: { position } },
-        { returnDocument: 'after', session },
-      )
-      // Not expected to happen, but type-safe
-      if (!task) throw new Error('failed to get updated task')
-
-      // Get subsequent tasks
-      const nextTasks = collection.find({
-        _herd: task._herd,
-        _id: { $ne: new ObjectId(id) },
-        position: { $gte: position },
-      }, { session }).sort('position', 1)
-
-      // Update subsequent tasks
-      let affectedCount = 1
-      for await (const task of nextTasks) {
-        await collection.updateOne(
-          { _id: task._id },
-          { $set: { position: position + affectedCount } },
-          { session },
-        )
-        affectedCount++
-      }
-
-      // Commit and return
-      await session.commitTransaction()
-      return { task, affectedCount }
-    } catch (err) {
-      await session.abortTransaction()
-      throw err
-    } finally {
-      await session.endSession()
-    }
-  }
-
   /**
    * Update a task.
    */

+ 6 - 0
src/types.ts

@@ -61,6 +61,12 @@ export interface Config {
 export interface Context {
   auth: Auth
   config: Config
+  /**
+   * Get the reference to the application context.
+   * This can be useful in cases where the context object passed through scope is not the same as the application
+   * context, such as inside of models.
+   */
+  ctx(): Context
   db: Db
   log: Logger
   model: Models