All files / flows delete.js

92.98% Statements 53/57
79.16% Branches 19/24
80% Functions 8/10
96.15% Lines 50/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 1731x             1x   1x 1x   10x           16x 4x 4x 4x 4x 4x     4x       4x 4x 4x               1x 10x 10x 10x   10x 1x             9x 1x               8x             7x 1x             6x 1x               5x               5x   5x 1x 1x 1x               1x 1x   1x   1x         5x   1x       5x               5x   1x       5x               5x   1x       5x             5x           1x 1x              
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb')
const {
  DynamoDBDocumentClient,
  GetCommand,
  DeleteCommand,
  QueryCommand,
  BatchWriteCommand
} = require('@aws-sdk/lib-dynamodb')
 
const client = new DynamoDBClient({})
const docClient = DynamoDBDocumentClient.from(client)
 
const getCorsHeaders = () => ({
  'Content-Type': 'application/json',
  'Access-Control-Allow-Origin': '*'
})
 
async function batchDelete(tableName, keys) {
  if (keys.length === 0) return
  for (let i = 0; i < keys.length; i += 25) {
    let unprocessed = keys.slice(i, i + 25)
    let attempts = 0
    while (unprocessed.length > 0 && attempts < 4) {
      const result = await docClient.send(
        new BatchWriteCommand({
          RequestItems: {
            [tableName]: unprocessed.map((key) => ({ DeleteRequest: { Key: key } }))
          }
        })
      )
      const remaining = result.UnprocessedItems?.[tableName]
      unprocessed = remaining?.map((r) => r.DeleteRequest.Key) ?? []
      Iif (unprocessed.length > 0) {
        await new Promise((r) => setTimeout(r, 200 * 2 ** attempts))
        attempts++
      }
    }
  }
}
 
exports.handler = async (event) => {
  try {
    const flowId = event.pathParameters?.flowId
    const userId = event.requestContext?.authorizer?.claims?.sub
 
    if (!userId) {
      return {
        statusCode: 401,
        headers: getCorsHeaders(),
        body: JSON.stringify({ error: 'Not authenticated' })
      }
    }
 
    if (!flowId) {
      return {
        statusCode: 400,
        headers: getCorsHeaders(),
        body: JSON.stringify({ error: 'Missing flowId' })
      }
    }
 
    // Verify flow exists and caller is the owner
    const flowResult = await docClient.send(
      new GetCommand({
        TableName: process.env.FLOWS_TABLE_NAME,
        Key: { id: flowId }
      })
    )
 
    if (!flowResult.Item) {
      return {
        statusCode: 404,
        headers: getCorsHeaders(),
        body: JSON.stringify({ error: 'Flow not found' })
      }
    }
 
    if (flowResult.Item.ownerId !== userId) {
      return {
        statusCode: 403,
        headers: getCorsHeaders(),
        body: JSON.stringify({ error: 'Only the flow owner can delete the flow' })
      }
    }
 
    // 1. Delete all items for each entity in this flow
    const entitiesResult = await docClient.send(
      new QueryCommand({
        TableName: process.env.ENTITIES_TABLE_NAME,
        IndexName: 'flowIndex',
        KeyConditionExpression: 'flowId = :flowId',
        ExpressionAttributeValues: { ':flowId': flowId }
      })
    )
    const entities = entitiesResult.Items ?? []
 
    for (const entity of entities) {
      let lastKey = undefined
      do {
        const itemsResult = await docClient.send(
          new QueryCommand({
            TableName: process.env.ITEMS_TABLE_NAME,
            KeyConditionExpression: 'entityId = :entityId',
            ExpressionAttributeValues: { ':entityId': entity.id },
            ExclusiveStartKey: lastKey
          })
        )
        const items = itemsResult.Items ?? []
        await batchDelete(
          process.env.ITEMS_TABLE_NAME,
          items.map((item) => ({ entityId: item.entityId, id: item.id }))
        )
        lastKey = itemsResult.LastEvaluatedKey
      } while (lastKey)
    }
 
    // 2. Delete all entities
    await batchDelete(
      process.env.ENTITIES_TABLE_NAME,
      entities.map((e) => ({ id: e.id }))
    )
 
    // 3. Delete all roles
    const rolesResult = await docClient.send(
      new QueryCommand({
        TableName: process.env.ROLES_TABLE_NAME,
        IndexName: 'flowIndex',
        KeyConditionExpression: 'flowId = :flowId',
        ExpressionAttributeValues: { ':flowId': flowId }
      })
    )
    await batchDelete(
      process.env.ROLES_TABLE_NAME,
      (rolesResult.Items ?? []).map((r) => ({ PK: r.PK, SK: r.SK }))
    )
 
    // 4. Delete all memberships
    const membershipsResult = await docClient.send(
      new QueryCommand({
        TableName: process.env.FLOW_MEMBERSHIPS_TABLE_NAME,
        IndexName: 'flowIndex',
        KeyConditionExpression: 'flowId = :flowId',
        ExpressionAttributeValues: { ':flowId': flowId }
      })
    )
    await batchDelete(
      process.env.FLOW_MEMBERSHIPS_TABLE_NAME,
      (membershipsResult.Items ?? []).map((m) => ({ userId: m.userId, flowId: m.flowId }))
    )
 
    // 5. Delete the flow itself
    await docClient.send(
      new DeleteCommand({
        TableName: process.env.FLOWS_TABLE_NAME,
        Key: { id: flowId }
      })
    )
 
    return {
      statusCode: 204,
      headers: getCorsHeaders(),
      body: ''
    }
  } catch (error) {
    console.error('Error deleting flow:', error)
    return {
      statusCode: 500,
      headers: getCorsHeaders(),
      body: JSON.stringify({ error: 'Failed to delete flow' })
    }
  }
}