All files / entities list.js

93.93% Statements 62/66
76.59% Branches 36/47
100% Functions 10/10
96.49% Lines 55/57

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 173 174 175 176 177 178 1791x 1x 1x   1x 1x     21x 21x 11x   10x 5x   6x 14x           8x 8x   2x 2x   2x 3x       13x   13x   13x                         13x     13x 13x 13x 13x   13x 1x                     12x                       11x 11x 10x               10x 10x   2x 2x 2x 2x   2x               2x 2x 2x 2x 2x                           11x 11x 8x       2x                   11x 11x 11x       11x                             1x 1x                     1x      
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb')
const { DynamoDBDocumentClient, QueryCommand, BatchGetCommand } = require('@aws-sdk/lib-dynamodb')
const { requirePermission } = require('../utils/requirePermission')
 
const client = new DynamoDBClient({})
const docClient = DynamoDBDocumentClient.from(client)
 
function extractSearchableText(value) {
  Iif (value === null || value === undefined) return []
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
    return [String(value)]
  }
  if (Array.isArray(value)) {
    return value.flatMap((entry) => extractSearchableText(entry))
  }
  Eif (typeof value === 'object') {
    return Object.values(value).flatMap((entry) => extractSearchableText(entry))
  }
  return []
}
 
function matchesQuery(value, query) {
  const normalizedQuery = typeof query === 'string' ? query.trim().toLowerCase() : ''
  if (!normalizedQuery) return true
 
  const terms = normalizedQuery.split(/\s+/).filter(Boolean)
  Iif (terms.length === 0) return true
 
  const haystack = extractSearchableText(value).join(' ').toLowerCase()
  return terms.every((term) => haystack.includes(term))
}
 
async function getEntitiesHandler(event) {
  try {
    // Extract flowId from JWT claims
    const flowId = event.requestContext?.authorizer?.claims?.['custom:flowId']
 
    Iif (!flowId) {
      return {
        statusCode: 401,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({ error: 'Missing flow context' })
      }
    }
 
    // Pagination support - Note: For simplicity, we load all and paginate in memory
    // For large datasets, consider using DynamoDB pagination on each query separately
    const requestedLimit = event.queryStringParameters?.limit
      ? parseInt(event.queryStringParameters.limit, 10)
      : 30
    const limit = Number.isNaN(requestedLimit) ? 30 : Math.min(Math.max(requestedLimit, 1), 100)
    const nextToken = event.queryStringParameters?.nextToken
    const search = event.queryStringParameters?.search
    const offset = nextToken ? parseInt(Buffer.from(nextToken, 'base64').toString(), 10) : 0
 
    if (Number.isNaN(offset) || offset < 0) {
      return {
        statusCode: 400,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({ error: 'Invalid nextToken' })
      }
    }
 
    // Query only this flow's own entities
    const flowEntities = await docClient.send(
      new QueryCommand({
        TableName: process.env.TABLE_NAME,
        IndexName: 'flowIndex',
        KeyConditionExpression: 'flowId = :flowId',
        ExpressionAttributeValues: {
          ':flowId': flowId
        }
      })
    )
 
    // Query linked entities for this flow (if EntityLinksTable is configured)
    const linkedEntities = []
    if (process.env.ENTITY_LINKS_TABLE_NAME) {
      const linksResult = await docClient.send(
        new QueryCommand({
          TableName: process.env.ENTITY_LINKS_TABLE_NAME,
          KeyConditionExpression: 'flowId = :flowId',
          ExpressionAttributeValues: { ':flowId': flowId }
        })
      )
 
      const links = linksResult.Items || []
      if (links.length > 0) {
        // Batch-get linked entities in chunks of 25 (DynamoDB safe limit)
        const BATCH_SIZE = 25
        for (let i = 0; i < links.length; i += BATCH_SIZE) {
          const chunk = links.slice(i, i + BATCH_SIZE)
          const keys = chunk.map((link) => ({ id: link.entityId }))
 
          const batchResult = await docClient.send(
            new BatchGetCommand({
              RequestItems: {
                [process.env.TABLE_NAME]: { Keys: keys }
              }
            })
          )
 
          const fetched = batchResult.Responses?.[process.env.TABLE_NAME] || []
          for (const entity of fetched) {
            const link = links.find((l) => l.entityId === entity.id)
            Eif (link) {
              linkedEntities.push({
                ...entity,
                linked: true,
                sourceFlowId: link.sourceFlowId,
                linkedAt: link.linkedAt,
                archivedAt: link.archivedAt || null
              })
            }
          }
        }
      }
    }
 
    // Own entities first, then linked entities
    const allEntities = [...(flowEntities.Items || []), ...linkedEntities]
    const filteredEntities = allEntities.filter((entity) =>
      matchesQuery(
        {
          name: entity.name,
          description: entity.description,
          fields: entity.fields?.map((field) => ({
            name: field.name,
            placeholder: field.placeholder,
            options: field.options
          }))
        },
        search
      )
    )
 
    const paginatedEntities = filteredEntities.slice(offset, offset + limit)
    const hasMore = offset + limit < filteredEntities.length
    const responseNextToken = hasMore
      ? Buffer.from(String(offset + limit)).toString('base64')
      : null
 
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({
        items: paginatedEntities,
        nextToken: responseNextToken,
        hasMore,
        filteredTotal: filteredEntities.length,
        total: allEntities.length
      })
    }
  } catch (error) {
    console.error('Error fetching entities:', error)
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({ error: 'Failed to fetch entities' })
    }
  }
}
 
exports.handler = requirePermission(getEntitiesHandler, {
  permission: 'entity:read'
})