Skip to main content

Entities List API

Overview​

The Entities List API allows authenticated users to retrieve a paginated list of entities (organizations and companies) from the Ring Platform. This endpoint supports filtering based on user access levels and provides comprehensive entity information including company details, social links, and visibility settings.

Endpoint Details​

  • URL: /api/entities
  • Method: GET
  • Authentication: Required (NextAuth.js session)
  • Content-Type: application/json

Authentication​

This endpoint requires user authentication via NextAuth.js session:

import { getSession } from 'next-auth/react'

const session = await getSession()
if (!session) {
// Handle unauthenticated state
}

Query Parameters​

ParameterTypeRequiredDescription
limitnumberNoNumber of entities to return (default: 20, max: 100)
startAfterstringNoCursor for pagination - entity ID to start after

Example Requests​

// Get first 20 entities
GET /api/entities

// Get next 10 entities after a specific entity
GET /api/entities?limit=10&startAfter=entity123

// Get maximum entities per page
GET /api/entities?limit=100

Response Format​

Success Response (200)​

{
"entities": [
{
"id": "entity123",
"name": "TechCorp AI Solutions",
"description": "Leading AI and machine learning solutions provider",
"industry": "ai-ml",
"founded": 2020,
"employeeCount": "50-100",
"website": "https://techcorp.ai",
"location": {
"city": "San Francisco",
"country": "USA",
"timezone": "America/Los_Angeles"
},
"socialLinks": {
"linkedin": "https://linkedin.com/company/techcorp-ai",
"twitter": "https://twitter.com/techcorp_ai",
"facebook": null,
"instagram": null
},
"visibility": "public",
"certifications": [
"ISO 27001",
"SOC 2 Type II"
],
"partnerships": [
"Google Cloud Partner",
"AWS Advanced Partner"
],
"upcomingEvents": [
{
"title": "AI Innovation Summit 2024",
"date": "2024-03-15T09:00:00Z",
"location": "San Francisco, CA"
}
],
"logo": "https://storage.googleapis.com/ring-platform/logos/entity123.png",
"banner": "https://storage.googleapis.com/ring-platform/banners/entity123.jpg",
"verified": true,
"memberCount": 245,
"opportunityCount": 12,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T14:22:00Z"
}
],
"lastVisible": "entity123",
"hasMore": true,
"totalCount": 1247
}

Empty Response (200)​

{
"entities": [],
"lastVisible": null,
"hasMore": false,
"totalCount": 0
}

Error Responses​

Unauthorized (401)​

{
"error": "Unauthorized"
}

Forbidden (403)​

{
"error": "Permission denied to access entities"
}

Internal Server Error (500)​

{
"error": "Internal Server Error"
}

Implementation Examples​

React Component with Pagination​

import { useState, useEffect, useCallback } from 'react'
import { useSession } from 'next-auth/react'

interface Entity {
id: string
name: string
description: string
industry: string
website: string
logo: string
verified: boolean
memberCount: number
opportunityCount: number
}

interface EntitiesResponse {
entities: Entity[]
lastVisible: string | null
hasMore: boolean
totalCount: number
}

function EntitiesList() {
const { data: session } = useSession()
const [entities, setEntities] = useState<Entity[]>([])
const [loading, setLoading] = useState(true)
const [loadingMore, setLoadingMore] = useState(false)
const [error, setError] = useState<string | null>(null)
const [lastVisible, setLastVisible] = useState<string | null>(null)
const [hasMore, setHasMore] = useState(true)

const fetchEntities = useCallback(async (reset = false) => {
if (!session) return

if (reset) {
setLoading(true)
setEntities([])
setLastVisible(null)
} else {
setLoadingMore(true)
}

setError(null)

try {
const params = new URLSearchParams({
limit: '20'
})

if (!reset && lastVisible) {
params.append('startAfter', lastVisible)
}

const response = await fetch(`/api/entities?${params}`)

if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to fetch entities')
}

const data: EntitiesResponse = await response.json()

if (reset) {
setEntities(data.entities)
} else {
setEntities(prev => [...prev, ...data.entities])
}

setLastVisible(data.lastVisible)
setHasMore(data.hasMore)

} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error occurred')
} finally {
setLoading(false)
setLoadingMore(false)
}
}, [session, lastVisible])

useEffect(() => {
fetchEntities(true)
}, [session])

const handleLoadMore = () => {
if (!loadingMore && hasMore) {
fetchEntities(false)
}
}

if (loading) {
return <div className="flex justify-center p-8">Loading entities...</div>
}

if (error) {
return (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
Error: {error}
</div>
)
}

return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{entities.map((entity) => (
<div key={entity.id} className="bg-white border rounded-lg shadow-sm p-6">
<div className="flex items-start space-x-4">
{entity.logo && (
<img
src={entity.logo}
alt={`${entity.name} logo`}
className="w-12 h-12 rounded-lg object-cover"
/>
)}
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{entity.name}
</h3>
{entity.verified && (
<span className="flex-shrink-0 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
Verified
</span>
)}
</div>
<p className="text-sm text-gray-500 mt-1 line-clamp-2">
{entity.description}
</p>
<div className="flex items-center justify-between mt-4 text-sm text-gray-500">
<span>{entity.memberCount} members</span>
<span>{entity.opportunityCount} opportunities</span>
</div>
</div>
</div>
</div>
))}
</div>

{hasMore && (
<div className="flex justify-center">
<button
onClick={handleLoadMore}
disabled={loadingMore}
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50"
>
{loadingMore ? 'Loading...' : 'Load More'}
</button>
</div>
)}
</div>
)
}

Custom Hook for Entity Management​

import { useState, useEffect, useCallback } from 'react'
import { useSession } from 'next-auth/react'

export function useEntities(initialLimit = 20) {
const { data: session } = useSession()
const [entities, setEntities] = useState<Entity[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [lastVisible, setLastVisible] = useState<string | null>(null)
const [hasMore, setHasMore] = useState(true)
const [totalCount, setTotalCount] = useState(0)

const fetchEntities = useCallback(async (reset = false) => {
if (!session) return

try {
const params = new URLSearchParams({
limit: initialLimit.toString()
})

if (!reset && lastVisible) {
params.append('startAfter', lastVisible)
}

const response = await fetch(`/api/entities?${params}`)

if (!response.ok) {
throw new Error('Failed to fetch entities')
}

const data: EntitiesResponse = await response.json()

if (reset) {
setEntities(data.entities)
} else {
setEntities(prev => [...prev, ...data.entities])
}

setLastVisible(data.lastVisible)
setHasMore(data.hasMore)
setTotalCount(data.totalCount)

} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setLoading(false)
}
}, [session, initialLimit, lastVisible])

const loadMore = useCallback(() => {
if (hasMore && !loading) {
fetchEntities(false)
}
}, [hasMore, loading, fetchEntities])

const refresh = useCallback(() => {
setLoading(true)
setError(null)
setLastVisible(null)
fetchEntities(true)
}, [fetchEntities])

useEffect(() => {
fetchEntities(true)
}, [session])

return {
entities,
loading,
error,
hasMore,
totalCount,
loadMore,
refresh
}
}

Server-Side Rendering (Next.js)​

import { GetServerSideProps } from 'next'
import { getServerAuthSession } from '@/auth'

interface EntitiesPageProps {
initialEntities: Entity[]
initialLastVisible: string | null
initialHasMore: boolean
totalCount: number
}

export default function EntitiesPage({
initialEntities,
initialLastVisible,
initialHasMore,
totalCount
}: EntitiesPageProps) {
const [entities, setEntities] = useState(initialEntities)
const [lastVisible, setLastVisible] = useState(initialLastVisible)
const [hasMore, setHasMore] = useState(initialHasMore)

return (
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold text-gray-900">
Entities Directory
</h1>
<span className="text-sm text-gray-500">
{totalCount.toLocaleString()} total entities
</span>
</div>

<EntitiesList
entities={entities}
lastVisible={lastVisible}
hasMore={hasMore}
onUpdate={setEntities}
/>
</div>
)
}

export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getServerAuthSession(context)

if (!session) {
return {
redirect: {
destination: '/auth/signin',
permanent: false,
},
}
}

try {
// Fetch initial entities server-side
const response = await fetch(`${process.env.NEXTAUTH_URL}/api/entities?limit=20`, {
headers: {
'Cookie': context.req.headers.cookie || '',
},
})

if (!response.ok) {
throw new Error('Failed to fetch entities')
}

const data = await response.json()

return {
props: {
initialEntities: data.entities,
initialLastVisible: data.lastVisible,
initialHasMore: data.hasMore,
totalCount: data.totalCount
},
}
} catch (error) {
return {
props: {
initialEntities: [],
initialLastVisible: null,
initialHasMore: false,
totalCount: 0
},
}
}
}

cURL Example​

# Get first page of entities
curl -X GET https://ring.ck.ua/api/entities \
-H "Cookie: next-auth.session-token=your-session-token"

# Get specific number of entities
curl -X GET "https://ring.ck.ua/api/entities?limit=50" \
-H "Cookie: next-auth.session-token=your-session-token"

# Get next page of entities
curl -X GET "https://ring.ck.ua/api/entities?limit=20&startAfter=entity123" \
-H "Cookie: next-auth.session-token=your-session-token"

Business Logic​

The endpoint performs the following operations:

  1. Authentication Check: Verifies user session and extracts user role
  2. Role-Based Filtering: Applies visibility filters based on user access level
  3. Pagination Setup: Configures limit and cursor-based pagination
  4. Database Query: Fetches entities from Firestore with proper ordering
  5. Response Formatting: Returns entities with pagination metadata

Access Control​

Visibility Levels​

  • Public: Available to all authenticated users
  • Subscriber: Available to subscribers and above
  • Member: Available to members and above
  • Confidential: Available to confidential level and above

Role-Based Access​

const getVisibilityFilter = (userRole: UserRole) => {
switch (userRole) {
case UserRole.ADMIN:
case UserRole.CONFIDENTIAL:
return ['public', 'subscriber', 'member', 'confidential']
case UserRole.MEMBER:
return ['public', 'subscriber', 'member']
case UserRole.SUBSCRIBER:
return ['public', 'subscriber']
default:
return ['public']
}
}

Performance Optimizations​

  • Firestore Indexing: Entities are indexed by visibility and creation date
  • Pagination: Cursor-based pagination for efficient large dataset handling
  • Caching: Response caching with appropriate cache headers
  • Field Selection: Only necessary fields are returned to minimize bandwidth

Rate Limiting​

  • User-based: 60 requests per minute per authenticated user
  • Global: 1000 requests per minute across all users

Database Query Optimization​

// Optimized Firestore query
const entitiesQuery = db
.collection('entities')
.where('visibility', 'in', visibilityLevels)
.orderBy('createdAt', 'desc')
.limit(limit)

if (startAfter) {
const startDoc = await db.collection('entities').doc(startAfter).get()
entitiesQuery = entitiesQuery.startAfter(startDoc)
}

Testing​

Unit Test Example​

import { GET } from '@/app/api/entities/route'
import { NextRequest } from 'next/server'

jest.mock('@/auth')
jest.mock('@/services/entities/get-entities')

describe('/api/entities', () => {
it('should return entities successfully', async () => {
(getServerAuthSession as jest.Mock).mockResolvedValue({
user: { id: 'user123', role: 'member' }
})

const mockEntities = [
{ id: 'entity1', name: 'Test Entity 1', visibility: 'public' },
{ id: 'entity2', name: 'Test Entity 2', visibility: 'member' }
]

(getEntities as jest.Mock).mockResolvedValue({
entities: mockEntities,
lastVisible: 'entity2'
})

const request = new NextRequest('http://localhost:3000/api/entities')
const response = await GET(request)
const data = await response.json()

expect(response.status).toBe(200)
expect(data.entities).toHaveLength(2)
expect(data.lastVisible).toBe('entity2')
})

it('should handle pagination correctly', async () => {
const request = new NextRequest('http://localhost:3000/api/entities?limit=10&startAfter=entity123')
const response = await GET(request)

expect(getEntities).toHaveBeenCalledWith(10, 'entity123')
})
})