**feat(storage): add pagination and filtering support for nouns and verbs**

- Introduced `PaginationOptions`, `NounFilterOptions`, and `VerbFilterOptions` types for improved query flexibility in data retrieval operations.
- Added `getNouns` and `getVerbs` methods with pagination and filtering capabilities, replacing existing methods for broader use cases and scalability.
- Marked legacy methods (`getAllNouns`, `getAllVerbs`, `getVerbsBySource`, `getVerbsByTarget`, `getVerbsByType`) as deprecated, directing users to use new methods.
- Updated `coreTypes`, `memoryStorage`, and related modules to support new functionality, including cursor and offset-based pagination handling.
- Updated fallback logic for storage adapters, ensuring compatibility with non-paginated operations when required.

**Purpose**: Enhance scalability and query precision by implementing paginated and filtered retrieval of nouns and verbs, aligning query methods with modern requirements.
This commit is contained in:
David Snelling 2025-07-31 13:13:15 -07:00
parent 59caa6ab5b
commit c3c4ca31e1
8 changed files with 1309 additions and 143 deletions

View file

@ -11,21 +11,37 @@ import { StatisticsData, StorageAdapter } from '../../coreTypes.js'
export abstract class BaseStorageAdapter implements StorageAdapter {
// Abstract methods that must be implemented by subclasses
abstract init(): Promise<void>
abstract saveNoun(noun: any): Promise<void>
abstract getNoun(id: string): Promise<any | null>
abstract getAllNouns(): Promise<any[]>
abstract getNounsByNounType(nounType: string): Promise<any[]>
abstract deleteNoun(id: string): Promise<void>
abstract saveVerb(verb: any): Promise<void>
abstract getVerb(id: string): Promise<any | null>
abstract getAllVerbs(): Promise<any[]>
abstract getVerbsBySource(sourceId: string): Promise<any[]>
abstract getVerbsByTarget(targetId: string): Promise<any[]>
abstract getVerbsByType(type: string): Promise<any[]>
abstract deleteVerb(id: string): Promise<void>
abstract saveMetadata(id: string, metadata: any): Promise<void>
abstract getMetadata(id: string): Promise<any | null>
abstract clear(): Promise<void>
abstract getStorageStatus(): Promise<{
type: string
used: number
@ -33,26 +49,77 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
details?: Record<string, any>
}>
/**
* Get nouns with pagination and filtering
* @param options Pagination and filtering options
* @returns Promise that resolves to a paginated result of nouns
*/
abstract getNouns(options?: {
pagination?: {
offset?: number
limit?: number
cursor?: string
}
filter?: {
nounType?: string | string[]
service?: string | string[]
metadata?: Record<string, any>
}
}): Promise<{
items: any[]
totalCount?: number
hasMore: boolean
nextCursor?: string
}>
/**
* Get verbs with pagination and filtering
* @param options Pagination and filtering options
* @returns Promise that resolves to a paginated result of verbs
*/
abstract getVerbs(options?: {
pagination?: {
offset?: number
limit?: number
cursor?: string
}
filter?: {
verbType?: string | string[]
sourceId?: string | string[]
targetId?: string | string[]
service?: string | string[]
metadata?: Record<string, any>
}
}): Promise<{
items: any[]
totalCount?: number
hasMore: boolean
nextCursor?: string
}>
// Statistics cache
protected statisticsCache: StatisticsData | null = null
// Batch update timer ID
protected statisticsBatchUpdateTimerId: NodeJS.Timeout | null = null
// Flag to indicate if statistics have been modified since last save
protected statisticsModified = false
// Time of last statistics flush to storage
protected lastStatisticsFlushTime = 0
// Minimum time between statistics flushes (5 seconds)
protected readonly MIN_FLUSH_INTERVAL_MS = 5000
// Maximum time to wait before flushing statistics (30 seconds)
protected readonly MAX_FLUSH_DELAY_MS = 30000
// Statistics-specific methods that must be implemented by subclasses
protected abstract saveStatisticsData(statistics: StatisticsData): Promise<void>
protected abstract saveStatisticsData(
statistics: StatisticsData
): Promise<void>
protected abstract getStatisticsData(): Promise<StatisticsData | null>
/**
@ -62,13 +129,13 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
async saveStatistics(statistics: StatisticsData): Promise<void> {
// Update the cache with a deep copy to avoid reference issues
this.statisticsCache = {
nounCount: {...statistics.nounCount},
verbCount: {...statistics.verbCount},
metadataCount: {...statistics.metadataCount},
nounCount: { ...statistics.nounCount },
verbCount: { ...statistics.verbCount },
metadataCount: { ...statistics.metadataCount },
hnswIndexSize: statistics.hnswIndexSize,
lastUpdated: statistics.lastUpdated
}
// Schedule a batch update instead of saving immediately
this.scheduleBatchUpdate()
}
@ -81,32 +148,32 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
// If we have cached statistics, return a deep copy
if (this.statisticsCache) {
return {
nounCount: {...this.statisticsCache.nounCount},
verbCount: {...this.statisticsCache.verbCount},
metadataCount: {...this.statisticsCache.metadataCount},
nounCount: { ...this.statisticsCache.nounCount },
verbCount: { ...this.statisticsCache.verbCount },
metadataCount: { ...this.statisticsCache.metadataCount },
hnswIndexSize: this.statisticsCache.hnswIndexSize,
lastUpdated: this.statisticsCache.lastUpdated
}
}
// Otherwise, get from storage
const statistics = await this.getStatisticsData()
// If we found statistics, update the cache
if (statistics) {
// Update the cache with a deep copy
this.statisticsCache = {
nounCount: {...statistics.nounCount},
verbCount: {...statistics.verbCount},
metadataCount: {...statistics.metadataCount},
nounCount: { ...statistics.nounCount },
verbCount: { ...statistics.verbCount },
metadataCount: { ...statistics.metadataCount },
hnswIndexSize: statistics.hnswIndexSize,
lastUpdated: statistics.lastUpdated
}
}
return statistics
}
/**
* Schedule a batch update of statistics
*/
@ -124,9 +191,10 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
const timeSinceLastFlush = now - this.lastStatisticsFlushTime
// If we've recently flushed, wait longer before the next flush
const delayMs = timeSinceLastFlush < this.MIN_FLUSH_INTERVAL_MS
? this.MAX_FLUSH_DELAY_MS
: this.MIN_FLUSH_INTERVAL_MS
const delayMs =
timeSinceLastFlush < this.MIN_FLUSH_INTERVAL_MS
? this.MAX_FLUSH_DELAY_MS
: this.MIN_FLUSH_INTERVAL_MS
// Schedule the batch update
this.statisticsBatchUpdateTimerId = setTimeout(() => {
@ -183,12 +251,12 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
if (!statistics) {
statistics = this.createDefaultStatistics()
}
// Update the cache
this.statisticsCache = {
nounCount: {...statistics.nounCount},
verbCount: {...statistics.verbCount},
metadataCount: {...statistics.metadataCount},
nounCount: { ...statistics.nounCount },
verbCount: { ...statistics.verbCount },
metadataCount: { ...statistics.metadataCount },
hnswIndexSize: statistics.hnswIndexSize,
lastUpdated: statistics.lastUpdated
}
@ -229,12 +297,12 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
if (!statistics) {
statistics = this.createDefaultStatistics()
}
// Update the cache
this.statisticsCache = {
nounCount: {...statistics.nounCount},
verbCount: {...statistics.verbCount},
metadataCount: {...statistics.metadataCount},
nounCount: { ...statistics.nounCount },
verbCount: { ...statistics.verbCount },
metadataCount: { ...statistics.metadataCount },
hnswIndexSize: statistics.hnswIndexSize,
lastUpdated: statistics.lastUpdated
}
@ -269,12 +337,12 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
if (!statistics) {
statistics = this.createDefaultStatistics()
}
// Update the cache
this.statisticsCache = {
nounCount: {...statistics.nounCount},
verbCount: {...statistics.verbCount},
metadataCount: {...statistics.metadataCount},
nounCount: { ...statistics.nounCount },
verbCount: { ...statistics.verbCount },
metadataCount: { ...statistics.metadataCount },
hnswIndexSize: statistics.hnswIndexSize,
lastUpdated: statistics.lastUpdated
}
@ -317,4 +385,4 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
lastUpdated: new Date().toISOString()
}
}
}
}

View file

@ -5,6 +5,7 @@
import { GraphVerb, HNSWNoun, StatisticsData } from '../../coreTypes.js'
import { BaseStorage, STATISTICS_KEY } from '../baseStorage.js'
import { PaginatedResult } from '../../types/paginationTypes.js'
// No type aliases needed - using the original types directly
@ -104,38 +105,124 @@ export class MemoryStorage extends BaseStorage {
return allNouns
}
/**
* Get nouns with pagination and filtering
* @param options Pagination and filtering options
* @returns Promise that resolves to a paginated result of nouns
*/
public async getNouns(options: {
pagination?: {
offset?: number
limit?: number
cursor?: string
}
filter?: {
nounType?: string | string[]
service?: string | string[]
metadata?: Record<string, any>
}
} = {}): Promise<PaginatedResult<HNSWNoun>> {
const pagination = options.pagination || {}
const filter = options.filter || {}
// Default values
const offset = pagination.offset || 0
const limit = pagination.limit || 100
// Convert string types to arrays for consistent handling
const nounTypes = filter.nounType
? Array.isArray(filter.nounType) ? filter.nounType : [filter.nounType]
: undefined
const services = filter.service
? Array.isArray(filter.service) ? filter.service : [filter.service]
: undefined
// First, collect all noun IDs that match the filter criteria
const matchingIds: string[] = []
// Iterate through all nouns to find matches
for (const [nounId, noun] of this.nouns.entries()) {
// Get the metadata to check filters
const metadata = await this.getMetadata(nounId)
if (!metadata) continue
// Filter by noun type if specified
if (nounTypes && !nounTypes.includes(metadata.noun)) {
continue
}
// Filter by service if specified
if (services && metadata.service && !services.includes(metadata.service)) {
continue
}
// Filter by metadata fields if specified
if (filter.metadata) {
let metadataMatch = true
for (const [key, value] of Object.entries(filter.metadata)) {
if (metadata[key] !== value) {
metadataMatch = false
break
}
}
if (!metadataMatch) continue
}
// If we got here, the noun matches all filters
matchingIds.push(nounId)
}
// Calculate pagination
const totalCount = matchingIds.length
const paginatedIds = matchingIds.slice(offset, offset + limit)
const hasMore = offset + limit < totalCount
// Create cursor for next page if there are more results
const nextCursor = hasMore ? `${offset + limit}` : undefined
// Fetch the actual nouns for the current page
const items: HNSWNoun[] = []
for (const id of paginatedIds) {
const noun = this.nouns.get(id)
if (!noun) continue
// Create a deep copy to avoid reference issues
const nounCopy: HNSWNoun = {
id: noun.id,
vector: [...noun.vector],
connections: new Map()
}
// Copy connections
for (const [level, connections] of noun.connections.entries()) {
nounCopy.connections.set(level, new Set(connections))
}
items.push(nounCopy)
}
return {
items,
totalCount,
hasMore,
nextCursor
}
}
/**
* Get nouns by noun type
* @param nounType The noun type to filter by
* @returns Promise that resolves to an array of nouns of the specified noun type
* @deprecated Use getNouns() with filter.nounType instead
*/
protected async getNounsByNounType_internal(nounType: string): Promise<HNSWNoun[]> {
const nouns: HNSWNoun[] = []
// Iterate through all nouns and filter by noun type using metadata
for (const [nounId, noun] of this.nouns.entries()) {
// Get the metadata to check the noun type
const metadata = await this.getMetadata(nounId)
// Include the noun if its noun type matches the requested type
if (metadata && metadata.noun === nounType) {
// Return a deep copy to avoid reference issues
const nounCopy: HNSWNoun = {
id: noun.id,
vector: [...noun.vector],
connections: new Map()
}
// Copy connections
for (const [level, connections] of noun.connections.entries()) {
nounCopy.connections.set(level, new Set(connections))
}
nouns.push(nounCopy)
const result = await this.getNouns({
filter: {
nounType
}
}
return nouns
})
return result.items
}
/**
@ -271,28 +358,176 @@ export class MemoryStorage extends BaseStorage {
return allVerbs
}
/**
* Get verbs with pagination and filtering
* @param options Pagination and filtering options
* @returns Promise that resolves to a paginated result of verbs
*/
public async getVerbs(options: {
pagination?: {
offset?: number
limit?: number
cursor?: string
}
filter?: {
verbType?: string | string[]
sourceId?: string | string[]
targetId?: string | string[]
service?: string | string[]
metadata?: Record<string, any>
}
} = {}): Promise<PaginatedResult<GraphVerb>> {
const pagination = options.pagination || {}
const filter = options.filter || {}
// Default values
const offset = pagination.offset || 0
const limit = pagination.limit || 100
// Convert string types to arrays for consistent handling
const verbTypes = filter.verbType
? Array.isArray(filter.verbType) ? filter.verbType : [filter.verbType]
: undefined
const sourceIds = filter.sourceId
? Array.isArray(filter.sourceId) ? filter.sourceId : [filter.sourceId]
: undefined
const targetIds = filter.targetId
? Array.isArray(filter.targetId) ? filter.targetId : [filter.targetId]
: undefined
const services = filter.service
? Array.isArray(filter.service) ? filter.service : [filter.service]
: undefined
// First, collect all verb IDs that match the filter criteria
const matchingIds: string[] = []
// Iterate through all verbs to find matches
for (const [verbId, verb] of this.verbs.entries()) {
// Filter by verb type if specified
if (verbTypes && !verbTypes.includes(verb.type || verb.verb || '')) {
continue
}
// Filter by source ID if specified
if (sourceIds && !sourceIds.includes(verb.sourceId || verb.source || '')) {
continue
}
// Filter by target ID if specified
if (targetIds && !targetIds.includes(verb.targetId || verb.target || '')) {
continue
}
// Filter by metadata fields if specified
if (filter.metadata && verb.metadata) {
let metadataMatch = true
for (const [key, value] of Object.entries(filter.metadata)) {
if (verb.metadata[key] !== value) {
metadataMatch = false
break
}
}
if (!metadataMatch) continue
}
// Filter by service if specified
if (services && verb.metadata && verb.metadata.service &&
!services.includes(verb.metadata.service)) {
continue
}
// If we got here, the verb matches all filters
matchingIds.push(verbId)
}
// Calculate pagination
const totalCount = matchingIds.length
const paginatedIds = matchingIds.slice(offset, offset + limit)
const hasMore = offset + limit < totalCount
// Create cursor for next page if there are more results
const nextCursor = hasMore ? `${offset + limit}` : undefined
// Fetch the actual verbs for the current page
const items: GraphVerb[] = []
for (const id of paginatedIds) {
const verb = this.verbs.get(id)
if (!verb) continue
// Create a deep copy to avoid reference issues
const verbCopy: GraphVerb = {
id: verb.id,
vector: [...verb.vector],
connections: new Map(),
sourceId: verb.sourceId || verb.source || '',
targetId: verb.targetId || verb.target || '',
source: verb.sourceId || verb.source || '',
target: verb.targetId || verb.target || '',
verb: verb.type || verb.verb,
type: verb.type || verb.verb,
weight: verb.weight,
metadata: verb.metadata ? JSON.parse(JSON.stringify(verb.metadata)) : undefined,
createdAt: verb.createdAt ? { ...verb.createdAt } : undefined,
updatedAt: verb.updatedAt ? { ...verb.updatedAt } : undefined,
createdBy: verb.createdBy ? { ...verb.createdBy } : undefined
}
// Copy connections
for (const [level, connections] of verb.connections.entries()) {
verbCopy.connections.set(level, new Set(connections))
}
items.push(verbCopy)
}
return {
items,
totalCount,
hasMore,
nextCursor
}
}
/**
* Get verbs by source
* @deprecated Use getVerbs() with filter.sourceId instead
*/
protected async getVerbsBySource_internal(sourceId: string): Promise<GraphVerb[]> {
const allVerbs = await this.getAllVerbs_internal()
return allVerbs.filter((verb: GraphVerb) => (verb.sourceId || verb.source) === sourceId)
const result = await this.getVerbs({
filter: {
sourceId
}
})
return result.items
}
/**
* Get verbs by target
* @deprecated Use getVerbs() with filter.targetId instead
*/
protected async getVerbsByTarget_internal(targetId: string): Promise<GraphVerb[]> {
const allVerbs = await this.getAllVerbs_internal()
return allVerbs.filter((verb: GraphVerb) => (verb.targetId || verb.target) === targetId)
const result = await this.getVerbs({
filter: {
targetId
}
})
return result.items
}
/**
* Get verbs by type
* @deprecated Use getVerbs() with filter.verbType instead
*/
protected async getVerbsByType_internal(type: string): Promise<GraphVerb[]> {
const allVerbs = await this.getAllVerbs_internal()
return allVerbs.filter((verb: GraphVerb) => (verb.type || verb.verb) === type)
const result = await this.getVerbs({
filter: {
verbType: type
}
})
return result.items
}
/**

View file

@ -124,6 +124,393 @@ export abstract class BaseStorage extends BaseStorageAdapter {
await this.ensureInitialized()
return this.getVerbsByType_internal(type)
}
/**
* Get nouns with pagination and filtering
* @param options Pagination and filtering options
* @returns Promise that resolves to a paginated result of nouns
*/
public async getNouns(options?: {
pagination?: {
offset?: number
limit?: number
cursor?: string
}
filter?: {
nounType?: string | string[]
service?: string | string[]
metadata?: Record<string, any>
}
}): Promise<{
items: HNSWNoun[]
totalCount?: number
hasMore: boolean
nextCursor?: string
}> {
await this.ensureInitialized()
// Set default pagination values
const pagination = options?.pagination || {}
const limit = pagination.limit || 100
const offset = pagination.offset || 0
// Optimize for common filter cases to avoid loading all nouns
if (options?.filter) {
// If filtering by nounType only, use the optimized method
if (options.filter.nounType && !options.filter.service && !options.filter.metadata) {
const nounType = Array.isArray(options.filter.nounType)
? options.filter.nounType[0]
: options.filter.nounType
// Get nouns by type directly
const nounsByType = await this.getNounsByNounType_internal(nounType)
// Apply pagination
const paginatedNouns = nounsByType.slice(offset, offset + limit)
const hasMore = offset + limit < nounsByType.length
// Set next cursor if there are more items
let nextCursor: string | undefined = undefined
if (hasMore && paginatedNouns.length > 0) {
const lastItem = paginatedNouns[paginatedNouns.length - 1]
nextCursor = lastItem.id
}
return {
items: paginatedNouns,
totalCount: nounsByType.length,
hasMore,
nextCursor
}
}
}
// For more complex filtering or no filtering, we need to get all nouns
// but limit the number we load to avoid memory issues
const maxNouns = offset + limit + 1 // Get one extra to check if there are more
let allNouns: HNSWNoun[] = []
try {
// Try to get only the nouns we need
allNouns = await this.getAllNouns_internal()
// If we have too many nouns, truncate the array to avoid memory issues
if (allNouns.length > maxNouns * 10) {
console.warn(`Large number of nouns (${allNouns.length}), truncating to ${maxNouns * 10} for filtering`)
allNouns = allNouns.slice(0, maxNouns * 10)
}
} catch (error) {
console.error('Error getting all nouns:', error)
// Return empty result on error
return {
items: [],
totalCount: 0,
hasMore: false
}
}
// Apply filtering if needed
let filteredNouns = allNouns
if (options?.filter) {
// Filter by noun type
if (options.filter.nounType) {
const nounTypes = Array.isArray(options.filter.nounType)
? options.filter.nounType
: [options.filter.nounType]
filteredNouns = filteredNouns.filter(noun => {
// HNSWNoun doesn't have a type property directly, check metadata
const nounType = noun.metadata?.type
return typeof nounType === 'string' && nounTypes.includes(nounType)
})
}
// Filter by service
if (options.filter.service) {
const services = Array.isArray(options.filter.service)
? options.filter.service
: [options.filter.service]
filteredNouns = filteredNouns.filter(noun => {
// HNSWNoun doesn't have a service property directly, check metadata
const service = noun.metadata?.service
return typeof service === 'string' && services.includes(service)
})
}
// Filter by metadata
if (options.filter.metadata) {
const metadataFilter = options.filter.metadata
filteredNouns = filteredNouns.filter(noun => {
if (!noun.metadata) return false
// Check if all metadata keys match
return Object.entries(metadataFilter).every(([key, value]) =>
noun.metadata && noun.metadata[key] === value
)
})
}
}
// Get total count before pagination
const totalCount = filteredNouns.length
// Apply pagination
const paginatedNouns = filteredNouns.slice(offset, offset + limit)
const hasMore = offset + limit < totalCount
// Set next cursor if there are more items
let nextCursor: string | undefined = undefined
if (hasMore && paginatedNouns.length > 0) {
const lastItem = paginatedNouns[paginatedNouns.length - 1]
nextCursor = lastItem.id
}
return {
items: paginatedNouns,
totalCount,
hasMore,
nextCursor
}
}
/**
* Get verbs with pagination and filtering
* @param options Pagination and filtering options
* @returns Promise that resolves to a paginated result of verbs
*/
public async getVerbs(options?: {
pagination?: {
offset?: number
limit?: number
cursor?: string
}
filter?: {
verbType?: string | string[]
sourceId?: string | string[]
targetId?: string | string[]
service?: string | string[]
metadata?: Record<string, any>
}
}): Promise<{
items: GraphVerb[]
totalCount?: number
hasMore: boolean
nextCursor?: string
}> {
await this.ensureInitialized()
// Set default pagination values
const pagination = options?.pagination || {}
const limit = pagination.limit || 100
const offset = pagination.offset || 0
// Optimize for common filter cases to avoid loading all verbs
if (options?.filter) {
// If filtering by sourceId only, use the optimized method
if (options.filter.sourceId && !options.filter.verbType &&
!options.filter.targetId && !options.filter.service &&
!options.filter.metadata) {
const sourceId = Array.isArray(options.filter.sourceId)
? options.filter.sourceId[0]
: options.filter.sourceId
// Get verbs by source directly
const verbsBySource = await this.getVerbsBySource_internal(sourceId)
// Apply pagination
const paginatedVerbs = verbsBySource.slice(offset, offset + limit)
const hasMore = offset + limit < verbsBySource.length
// Set next cursor if there are more items
let nextCursor: string | undefined = undefined
if (hasMore && paginatedVerbs.length > 0) {
const lastItem = paginatedVerbs[paginatedVerbs.length - 1]
nextCursor = lastItem.id
}
return {
items: paginatedVerbs,
totalCount: verbsBySource.length,
hasMore,
nextCursor
}
}
// If filtering by targetId only, use the optimized method
if (options.filter.targetId && !options.filter.verbType &&
!options.filter.sourceId && !options.filter.service &&
!options.filter.metadata) {
const targetId = Array.isArray(options.filter.targetId)
? options.filter.targetId[0]
: options.filter.targetId
// Get verbs by target directly
const verbsByTarget = await this.getVerbsByTarget_internal(targetId)
// Apply pagination
const paginatedVerbs = verbsByTarget.slice(offset, offset + limit)
const hasMore = offset + limit < verbsByTarget.length
// Set next cursor if there are more items
let nextCursor: string | undefined = undefined
if (hasMore && paginatedVerbs.length > 0) {
const lastItem = paginatedVerbs[paginatedVerbs.length - 1]
nextCursor = lastItem.id
}
return {
items: paginatedVerbs,
totalCount: verbsByTarget.length,
hasMore,
nextCursor
}
}
// If filtering by verbType only, use the optimized method
if (options.filter.verbType && !options.filter.sourceId &&
!options.filter.targetId && !options.filter.service &&
!options.filter.metadata) {
const verbType = Array.isArray(options.filter.verbType)
? options.filter.verbType[0]
: options.filter.verbType
// Get verbs by type directly
const verbsByType = await this.getVerbsByType_internal(verbType)
// Apply pagination
const paginatedVerbs = verbsByType.slice(offset, offset + limit)
const hasMore = offset + limit < verbsByType.length
// Set next cursor if there are more items
let nextCursor: string | undefined = undefined
if (hasMore && paginatedVerbs.length > 0) {
const lastItem = paginatedVerbs[paginatedVerbs.length - 1]
nextCursor = lastItem.id
}
return {
items: paginatedVerbs,
totalCount: verbsByType.length,
hasMore,
nextCursor
}
}
}
// For more complex filtering or no filtering, we need to get all verbs
// but limit the number we load to avoid memory issues
const maxVerbs = offset + limit + 1 // Get one extra to check if there are more
let allVerbs: GraphVerb[] = []
try {
// Try to get only the verbs we need
allVerbs = await this.getAllVerbs_internal()
// If we have too many verbs, truncate the array to avoid memory issues
if (allVerbs.length > maxVerbs * 10) {
console.warn(`Large number of verbs (${allVerbs.length}), truncating to ${maxVerbs * 10} for filtering`)
allVerbs = allVerbs.slice(0, maxVerbs * 10)
}
} catch (error) {
console.error('Error getting all verbs:', error)
// Return empty result on error
return {
items: [],
totalCount: 0,
hasMore: false
}
}
// Apply filtering if needed
let filteredVerbs = allVerbs
if (options?.filter) {
// Filter by verb type
if (options.filter.verbType) {
const verbTypes = Array.isArray(options.filter.verbType)
? options.filter.verbType
: [options.filter.verbType]
filteredVerbs = filteredVerbs.filter(verb =>
verb.type !== undefined && verbTypes.includes(verb.type)
)
}
// Filter by source ID
if (options.filter.sourceId) {
const sourceIds = Array.isArray(options.filter.sourceId)
? options.filter.sourceId
: [options.filter.sourceId]
filteredVerbs = filteredVerbs.filter(verb =>
verb.sourceId !== undefined && sourceIds.includes(verb.sourceId)
)
}
// Filter by target ID
if (options.filter.targetId) {
const targetIds = Array.isArray(options.filter.targetId)
? options.filter.targetId
: [options.filter.targetId]
filteredVerbs = filteredVerbs.filter(verb =>
verb.targetId !== undefined && targetIds.includes(verb.targetId)
)
}
// Filter by service
if (options.filter.service) {
const services = Array.isArray(options.filter.service)
? options.filter.service
: [options.filter.service]
filteredVerbs = filteredVerbs.filter(verb => {
// GraphVerb doesn't have a service property directly, check metadata
const service = verb.metadata?.service
return typeof service === 'string' && services.includes(service)
})
}
// Filter by metadata
if (options.filter.metadata) {
const metadataFilter = options.filter.metadata
filteredVerbs = filteredVerbs.filter(verb => {
if (!verb.metadata) return false
// Check if all metadata keys match
return Object.entries(metadataFilter).every(([key, value]) =>
verb.metadata && verb.metadata[key] === value
)
})
}
}
// Get total count before pagination
const totalCount = filteredVerbs.length
// Apply pagination
const paginatedVerbs = filteredVerbs.slice(offset, offset + limit)
const hasMore = offset + limit < totalCount
// Set next cursor if there are more items
let nextCursor: string | undefined = undefined
if (hasMore && paginatedVerbs.length > 0) {
const lastItem = paginatedVerbs[paginatedVerbs.length - 1]
nextCursor = lastItem.id
}
return {
items: paginatedVerbs,
totalCount,
hasMore,
nextCursor
}
}
/**
* Delete a verb from storage
@ -261,4 +648,4 @@ export abstract class BaseStorage extends BaseStorageAdapter {
* @returns Promise that resolves to the statistics data or null if not found
*/
protected abstract getStatisticsData(): Promise<StatisticsData | null>
}
}