**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:
parent
2b0f830d89
commit
cfa43a4160
8 changed files with 1309 additions and 143 deletions
|
|
@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Unified getNouns and getVerbs methods to improve code consistency
|
||||||
|
- Removed deprecated warnings from getAllNouns, getAllVerbs, getVerbsBySource, getVerbsByTarget, and getVerbsByType
|
||||||
|
- Implemented getAllNouns and getAllVerbs to use the paginated versions internally
|
||||||
|
- Improved method documentation with clearer parameter and return type descriptions
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1833,24 +1833,141 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nouns = this.index.getNouns()
|
// Use getNouns with no pagination to get all nouns
|
||||||
const result: VectorDocument<T>[] = []
|
const result = await this.getNouns({
|
||||||
|
pagination: {
|
||||||
for (const [id, noun] of nouns.entries()) {
|
limit: Number.MAX_SAFE_INTEGER // Request all nouns
|
||||||
const metadata = await this.storage!.getMetadata(id)
|
}
|
||||||
result.push({
|
})
|
||||||
id,
|
|
||||||
vector: noun.vector,
|
return result.items
|
||||||
metadata: metadata as T | undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get all nouns:', error)
|
console.error('Failed to get all nouns:', error)
|
||||||
throw new Error(`Failed to get all nouns: ${error}`)
|
throw new Error(`Failed to get all nouns: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get nouns with pagination and filtering
|
||||||
|
* @param options Pagination and filtering options
|
||||||
|
* @returns Paginated result of vector documents
|
||||||
|
*/
|
||||||
|
public async getNouns(options: {
|
||||||
|
pagination?: {
|
||||||
|
offset?: number
|
||||||
|
limit?: number
|
||||||
|
cursor?: string
|
||||||
|
}
|
||||||
|
filter?: {
|
||||||
|
nounType?: string | string[]
|
||||||
|
service?: string | string[]
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
} = {}): Promise<{
|
||||||
|
items: VectorDocument<T>[]
|
||||||
|
totalCount?: number
|
||||||
|
hasMore: boolean
|
||||||
|
nextCursor?: string
|
||||||
|
}> {
|
||||||
|
await this.ensureInitialized()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First try to use the storage adapter's paginated method
|
||||||
|
try {
|
||||||
|
const result = await this.storage!.getNouns(options)
|
||||||
|
|
||||||
|
// Convert HNSWNoun objects to VectorDocument objects
|
||||||
|
const items: VectorDocument<T>[] = []
|
||||||
|
|
||||||
|
for (const noun of result.items) {
|
||||||
|
const metadata = await this.storage!.getMetadata(noun.id)
|
||||||
|
items.push({
|
||||||
|
id: noun.id,
|
||||||
|
vector: noun.vector,
|
||||||
|
metadata: metadata as T | undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
totalCount: result.totalCount,
|
||||||
|
hasMore: result.hasMore,
|
||||||
|
nextCursor: result.nextCursor
|
||||||
|
}
|
||||||
|
} catch (storageError) {
|
||||||
|
// If storage adapter doesn't support pagination, fall back to using the index's paginated method
|
||||||
|
console.warn('Storage adapter does not support pagination, falling back to index pagination:', storageError)
|
||||||
|
|
||||||
|
const pagination = options.pagination || {}
|
||||||
|
const filter = options.filter || {}
|
||||||
|
|
||||||
|
// Create a filter function for the index
|
||||||
|
const filterFn = async (noun: HNSWNoun): Promise<boolean> => {
|
||||||
|
// If no filters, include all nouns
|
||||||
|
if (!filter.nounType && !filter.service && !filter.metadata) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get metadata for filtering
|
||||||
|
const metadata = await this.storage!.getMetadata(noun.id)
|
||||||
|
if (!metadata) return false
|
||||||
|
|
||||||
|
// Filter by noun type
|
||||||
|
if (filter.nounType) {
|
||||||
|
const nounTypes = Array.isArray(filter.nounType) ? filter.nounType : [filter.nounType]
|
||||||
|
if (!nounTypes.includes(metadata.noun)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by service
|
||||||
|
if (filter.service && metadata.service) {
|
||||||
|
const services = Array.isArray(filter.service) ? filter.service : [filter.service]
|
||||||
|
if (!services.includes(metadata.service)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by metadata fields
|
||||||
|
if (filter.metadata) {
|
||||||
|
for (const [key, value] of Object.entries(filter.metadata)) {
|
||||||
|
if (metadata[key] !== value) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filtered nouns from the index
|
||||||
|
// Note: We can't use async filter directly with getNounsPaginated, so we'll filter after
|
||||||
|
const indexResult = this.index.getNounsPaginated({
|
||||||
|
offset: pagination.offset,
|
||||||
|
limit: pagination.limit
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert to VectorDocument objects and apply filters
|
||||||
|
const items: VectorDocument<T>[] = []
|
||||||
|
|
||||||
|
for (const [id, noun] of indexResult.items.entries()) {
|
||||||
|
// Apply filter
|
||||||
|
if (await filterFn(noun)) {
|
||||||
|
const metadata = await this.storage!.getMetadata(id)
|
||||||
|
items.push({
|
||||||
|
id,
|
||||||
|
vector: noun.vector,
|
||||||
|
metadata: metadata as T | undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
totalCount: indexResult.totalCount, // This is approximate since we filter after pagination
|
||||||
|
hasMore: indexResult.hasMore,
|
||||||
|
nextCursor: pagination.cursor // Just pass through the cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get nouns with pagination:', error)
|
||||||
|
throw new Error(`Failed to get nouns with pagination: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a vector by ID
|
* Delete a vector by ID
|
||||||
|
|
@ -2341,26 +2458,84 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all verbs
|
* Get all verbs
|
||||||
|
* @returns Array of all verbs
|
||||||
*/
|
*/
|
||||||
public async getAllVerbs(): Promise<GraphVerb[]> {
|
public async getAllVerbs(): Promise<GraphVerb[]> {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.storage!.getAllVerbs()
|
// Use getVerbs with no pagination to get all verbs
|
||||||
|
const result = await this.getVerbs({
|
||||||
|
pagination: {
|
||||||
|
limit: Number.MAX_SAFE_INTEGER // Request all verbs
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.items
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get all verbs:', error)
|
console.error('Failed to get all verbs:', error)
|
||||||
throw new Error(`Failed to get all verbs: ${error}`)
|
throw new Error(`Failed to get all verbs: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get verbs with pagination and filtering
|
||||||
|
* @param options Pagination and filtering options
|
||||||
|
* @returns 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()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the storage adapter's paginated method
|
||||||
|
const result = await this.storage!.getVerbs(options)
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: result.items,
|
||||||
|
totalCount: result.totalCount,
|
||||||
|
hasMore: result.hasMore,
|
||||||
|
nextCursor: result.nextCursor
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get verbs with pagination:', error)
|
||||||
|
throw new Error(`Failed to get verbs with pagination: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get verbs by source noun ID
|
* Get verbs by source noun ID
|
||||||
|
* @param sourceId The ID of the source noun
|
||||||
|
* @returns Array of verbs originating from the specified source
|
||||||
*/
|
*/
|
||||||
public async getVerbsBySource(sourceId: string): Promise<GraphVerb[]> {
|
public async getVerbsBySource(sourceId: string): Promise<GraphVerb[]> {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.storage!.getVerbsBySource(sourceId)
|
// Use getVerbs with sourceId filter
|
||||||
|
const result = await this.getVerbs({
|
||||||
|
filter: {
|
||||||
|
sourceId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result.items
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get verbs by source ${sourceId}:`, error)
|
console.error(`Failed to get verbs by source ${sourceId}:`, error)
|
||||||
throw new Error(`Failed to get verbs by source ${sourceId}: ${error}`)
|
throw new Error(`Failed to get verbs by source ${sourceId}: ${error}`)
|
||||||
|
|
@ -2369,12 +2544,20 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get verbs by target noun ID
|
* Get verbs by target noun ID
|
||||||
|
* @param targetId The ID of the target noun
|
||||||
|
* @returns Array of verbs targeting the specified noun
|
||||||
*/
|
*/
|
||||||
public async getVerbsByTarget(targetId: string): Promise<GraphVerb[]> {
|
public async getVerbsByTarget(targetId: string): Promise<GraphVerb[]> {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.storage!.getVerbsByTarget(targetId)
|
// Use getVerbs with targetId filter
|
||||||
|
const result = await this.getVerbs({
|
||||||
|
filter: {
|
||||||
|
targetId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result.items
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get verbs by target ${targetId}:`, error)
|
console.error(`Failed to get verbs by target ${targetId}:`, error)
|
||||||
throw new Error(`Failed to get verbs by target ${targetId}: ${error}`)
|
throw new Error(`Failed to get verbs by target ${targetId}: ${error}`)
|
||||||
|
|
@ -2383,12 +2566,20 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get verbs by type
|
* Get verbs by type
|
||||||
|
* @param type The type of verb to retrieve
|
||||||
|
* @returns Array of verbs of the specified type
|
||||||
*/
|
*/
|
||||||
public async getVerbsByType(type: string): Promise<GraphVerb[]> {
|
public async getVerbsByType(type: string): Promise<GraphVerb[]> {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.storage!.getVerbsByType(type)
|
// Use getVerbs with verbType filter
|
||||||
|
const result = await this.getVerbs({
|
||||||
|
filter: {
|
||||||
|
verbType: type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result.items
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get verbs by type ${type}:`, error)
|
console.error(`Failed to get verbs by type ${type}:`, error)
|
||||||
throw new Error(`Failed to get verbs by type ${type}: ${error}`)
|
throw new Error(`Failed to get verbs by type ${type}: ${error}`)
|
||||||
|
|
@ -2467,24 +2658,64 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async getNounCount(): Promise<number> {
|
private async getNounCount(): Promise<number> {
|
||||||
// Get all verbs from storage
|
// Use the storage statistics if available
|
||||||
const allVerbs = await this.storage!.getAllVerbs()
|
try {
|
||||||
|
const stats = await this.storage!.getStatistics()
|
||||||
// Create a set of verb IDs for faster lookup
|
if (stats) {
|
||||||
const verbIds = new Set(allVerbs.map((verb) => verb.id))
|
// Calculate total noun count across all services
|
||||||
|
let totalNounCount = 0
|
||||||
// Get all nouns from the index
|
for (const serviceCount of Object.values(stats.nounCount)) {
|
||||||
const nouns = this.index.getNouns()
|
totalNounCount += serviceCount
|
||||||
|
}
|
||||||
// Count nouns that are not verbs
|
|
||||||
let nounCount = 0
|
// Calculate total verb count across all services
|
||||||
for (const [id] of nouns.entries()) {
|
let totalVerbCount = 0
|
||||||
if (!verbIds.has(id)) {
|
for (const serviceCount of Object.values(stats.verbCount)) {
|
||||||
nounCount++
|
totalVerbCount += serviceCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the difference (nouns excluding verbs)
|
||||||
|
return Math.max(0, totalNounCount - totalVerbCount)
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get statistics for noun count, falling back to paginated counting:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nounCount
|
// Fallback: Use paginated queries to count nouns and verbs
|
||||||
|
let nounCount = 0
|
||||||
|
let verbCount = 0
|
||||||
|
|
||||||
|
// Count all nouns using pagination
|
||||||
|
let hasMoreNouns = true
|
||||||
|
let offset = 0
|
||||||
|
const limit = 1000 // Use a larger limit for counting
|
||||||
|
|
||||||
|
while (hasMoreNouns) {
|
||||||
|
const result = await this.storage!.getNouns({
|
||||||
|
pagination: { offset, limit }
|
||||||
|
})
|
||||||
|
|
||||||
|
nounCount += result.items.length
|
||||||
|
hasMoreNouns = result.hasMore
|
||||||
|
offset += limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count all verbs using pagination
|
||||||
|
let hasMoreVerbs = true
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
while (hasMoreVerbs) {
|
||||||
|
const result = await this.storage!.getVerbs({
|
||||||
|
pagination: { offset, limit }
|
||||||
|
})
|
||||||
|
|
||||||
|
verbCount += result.items.length
|
||||||
|
hasMoreVerbs = result.hasMore
|
||||||
|
offset += limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the difference (nouns excluding verbs)
|
||||||
|
return Math.max(0, nounCount - verbCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2624,35 +2855,15 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// If statistics are not available, fall back to calculating them on-demand
|
// If statistics are not available, return zeros instead of calculating on-demand
|
||||||
console.warn('Persistent statistics not available, calculating on-demand')
|
console.warn('Persistent statistics not available, returning zeros')
|
||||||
|
|
||||||
// Get all verbs from storage
|
// Never use getVerbs and getNouns as fallback for getStatistics
|
||||||
const allVerbs = await this.storage!.getAllVerbs()
|
// as it's too expensive with millions of potential entries
|
||||||
const verbCount = allVerbs.length
|
const nounCount = 0
|
||||||
|
const verbCount = 0
|
||||||
// Get the noun count using the helper method
|
const metadataCount = 0
|
||||||
const nounCount = await this.getNounCount()
|
const hnswIndexSize = 0
|
||||||
|
|
||||||
// Count metadata entries by checking each noun for metadata
|
|
||||||
let metadataCount = 0
|
|
||||||
const nouns = this.index.getNouns()
|
|
||||||
for (const [id] of nouns.entries()) {
|
|
||||||
try {
|
|
||||||
const metadata = await this.storage!.getMetadata(id)
|
|
||||||
if (metadata !== null && metadata !== undefined) {
|
|
||||||
metadataCount++
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors when checking individual metadata entries
|
|
||||||
// This could happen if metadata is corrupted or missing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get HNSW index size (excluding verbs)
|
|
||||||
// The HNSW index includes both nouns and verbs, but for statistics we want to report
|
|
||||||
// only the number of actual nouns (excluding verbs) to match the expected behavior in tests
|
|
||||||
const hnswIndexSize = nounCount
|
|
||||||
|
|
||||||
// Create default statistics
|
// Create default statistics
|
||||||
const defaultStats = {
|
const defaultStats = {
|
||||||
|
|
|
||||||
113
src/coreTypes.ts
113
src/coreTypes.ts
|
|
@ -83,11 +83,11 @@ export interface GraphVerb extends HNSWNoun {
|
||||||
verb?: string // Alias for type
|
verb?: string // Alias for type
|
||||||
data?: Record<string, any> // Additional flexible data storage
|
data?: Record<string, any> // Additional flexible data storage
|
||||||
embedding?: Vector // Vector representation of the relationship
|
embedding?: Vector // Vector representation of the relationship
|
||||||
|
|
||||||
// Timestamp and creator properties
|
// Timestamp and creator properties
|
||||||
createdAt?: { seconds: number, nanoseconds: number } // When the verb was created
|
createdAt?: { seconds: number; nanoseconds: number } // When the verb was created
|
||||||
updatedAt?: { seconds: number, nanoseconds: number } // When the verb was last updated
|
updatedAt?: { seconds: number; nanoseconds: number } // When the verb was last updated
|
||||||
createdBy?: { augmentation: string, version: string } // Information about what created this verb
|
createdBy?: { augmentation: string; version: string } // Information about what created this verb
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -111,37 +111,37 @@ export interface StatisticsData {
|
||||||
* Count of nouns by service
|
* Count of nouns by service
|
||||||
*/
|
*/
|
||||||
nounCount: Record<string, number>
|
nounCount: Record<string, number>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count of verbs by service
|
* Count of verbs by service
|
||||||
*/
|
*/
|
||||||
verbCount: Record<string, number>
|
verbCount: Record<string, number>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count of metadata entries by service
|
* Count of metadata entries by service
|
||||||
*/
|
*/
|
||||||
metadataCount: Record<string, number>
|
metadataCount: Record<string, number>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Size of the HNSW index
|
* Size of the HNSW index
|
||||||
*/
|
*/
|
||||||
hnswIndexSize: number
|
hnswIndexSize: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Total number of nodes
|
* Total number of nodes
|
||||||
*/
|
*/
|
||||||
totalNodes?: number
|
totalNodes?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Total number of edges
|
* Total number of edges
|
||||||
*/
|
*/
|
||||||
totalEdges?: number
|
totalEdges?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Total metadata count
|
* Total metadata count
|
||||||
*/
|
*/
|
||||||
totalMetadata?: number
|
totalMetadata?: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operation counts
|
* Operation counts
|
||||||
*/
|
*/
|
||||||
|
|
@ -153,7 +153,7 @@ export interface StatisticsData {
|
||||||
relate: number
|
relate: number
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last updated timestamp
|
* Last updated timestamp
|
||||||
*/
|
*/
|
||||||
|
|
@ -167,12 +167,41 @@ export interface StorageAdapter {
|
||||||
|
|
||||||
getNoun(id: string): Promise<HNSWNoun | null>
|
getNoun(id: string): Promise<HNSWNoun | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all nouns from storage
|
||||||
|
* @deprecated Use getNouns() with pagination instead for better scalability
|
||||||
|
* @returns Promise that resolves to an array of all nouns
|
||||||
|
*/
|
||||||
getAllNouns(): Promise<HNSWNoun[]>
|
getAllNouns(): Promise<HNSWNoun[]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get nouns with pagination and filtering
|
||||||
|
* @param options Pagination and filtering options
|
||||||
|
* @returns Promise that resolves to a paginated result of nouns
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get nouns by noun type
|
* Get nouns by noun type
|
||||||
* @param nounType The noun type to filter by
|
* @param nounType The noun type to filter by
|
||||||
* @returns Promise that resolves to an array of nouns of the specified noun type
|
* @returns Promise that resolves to an array of nouns of the specified noun type
|
||||||
|
* @deprecated Use getNouns() with filter.nounType instead
|
||||||
*/
|
*/
|
||||||
getNounsByNounType(nounType: string): Promise<HNSWNoun[]>
|
getNounsByNounType(nounType: string): Promise<HNSWNoun[]>
|
||||||
|
|
||||||
|
|
@ -182,12 +211,60 @@ export interface StorageAdapter {
|
||||||
|
|
||||||
getVerb(id: string): Promise<GraphVerb | null>
|
getVerb(id: string): Promise<GraphVerb | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all verbs from storage
|
||||||
|
* @deprecated Use getVerbs() with pagination instead for better scalability
|
||||||
|
* @returns Promise that resolves to an array of all verbs
|
||||||
|
*/
|
||||||
getAllVerbs(): Promise<GraphVerb[]>
|
getAllVerbs(): Promise<GraphVerb[]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get verbs with pagination and filtering
|
||||||
|
* @param options Pagination and filtering options
|
||||||
|
* @returns Promise that resolves to a paginated result of verbs
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get verbs by source
|
||||||
|
* @param sourceId The source ID to filter by
|
||||||
|
* @returns Promise that resolves to an array of verbs with the specified source ID
|
||||||
|
* @deprecated Use getVerbs() with filter.sourceId instead
|
||||||
|
*/
|
||||||
getVerbsBySource(sourceId: string): Promise<GraphVerb[]>
|
getVerbsBySource(sourceId: string): Promise<GraphVerb[]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get verbs by target
|
||||||
|
* @param targetId The target ID to filter by
|
||||||
|
* @returns Promise that resolves to an array of verbs with the specified target ID
|
||||||
|
* @deprecated Use getVerbs() with filter.targetId instead
|
||||||
|
*/
|
||||||
getVerbsByTarget(targetId: string): Promise<GraphVerb[]>
|
getVerbsByTarget(targetId: string): Promise<GraphVerb[]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get verbs by type
|
||||||
|
* @param type The verb type to filter by
|
||||||
|
* @returns Promise that resolves to an array of verbs with the specified type
|
||||||
|
* @deprecated Use getVerbs() with filter.verbType instead
|
||||||
|
*/
|
||||||
getVerbsByType(type: string): Promise<GraphVerb[]>
|
getVerbsByType(type: string): Promise<GraphVerb[]>
|
||||||
|
|
||||||
deleteVerb(id: string): Promise<void>
|
deleteVerb(id: string): Promise<void>
|
||||||
|
|
@ -242,7 +319,11 @@ export interface StorageAdapter {
|
||||||
* @param service The service that inserted the data
|
* @param service The service that inserted the data
|
||||||
* @param amount The amount to increment by (default: 1)
|
* @param amount The amount to increment by (default: 1)
|
||||||
*/
|
*/
|
||||||
incrementStatistic(type: 'noun' | 'verb' | 'metadata', service: string, amount?: number): Promise<void>
|
incrementStatistic(
|
||||||
|
type: 'noun' | 'verb' | 'metadata',
|
||||||
|
service: string,
|
||||||
|
amount?: number
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrement a statistic counter
|
* Decrement a statistic counter
|
||||||
|
|
@ -250,7 +331,11 @@ export interface StorageAdapter {
|
||||||
* @param service The service that inserted the data
|
* @param service The service that inserted the data
|
||||||
* @param amount The amount to decrement by (default: 1)
|
* @param amount The amount to decrement by (default: 1)
|
||||||
*/
|
*/
|
||||||
decrementStatistic(type: 'noun' | 'verb' | 'metadata', service: string, amount?: number): Promise<void>
|
decrementStatistic(
|
||||||
|
type: 'noun' | 'verb' | 'metadata',
|
||||||
|
service: string,
|
||||||
|
amount?: number
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the HNSW index size statistic
|
* Update the HNSW index size statistic
|
||||||
|
|
|
||||||
|
|
@ -456,11 +456,57 @@ export class HNSWIndex {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all nouns in the index
|
* Get all nouns in the index
|
||||||
|
* @deprecated Use getNounsPaginated() instead for better scalability
|
||||||
*/
|
*/
|
||||||
public getNouns(): Map<string, HNSWNoun> {
|
public getNouns(): Map<string, HNSWNoun> {
|
||||||
return new Map(this.nouns)
|
return new Map(this.nouns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get nouns with pagination
|
||||||
|
* @param options Pagination options
|
||||||
|
* @returns Object containing paginated nouns and pagination info
|
||||||
|
*/
|
||||||
|
public getNounsPaginated(
|
||||||
|
options: {
|
||||||
|
offset?: number
|
||||||
|
limit?: number
|
||||||
|
filter?: (noun: HNSWNoun) => boolean
|
||||||
|
} = {}
|
||||||
|
): {
|
||||||
|
items: Map<string, HNSWNoun>
|
||||||
|
totalCount: number
|
||||||
|
hasMore: boolean
|
||||||
|
} {
|
||||||
|
const offset = options.offset || 0
|
||||||
|
const limit = options.limit || 100
|
||||||
|
const filter = options.filter || (() => true)
|
||||||
|
|
||||||
|
// Get all noun entries
|
||||||
|
const entries = [...this.nouns.entries()]
|
||||||
|
|
||||||
|
// Apply filter if provided
|
||||||
|
const filteredEntries = entries.filter(([_, noun]) => filter(noun))
|
||||||
|
|
||||||
|
// Get total count after filtering
|
||||||
|
const totalCount = filteredEntries.length
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
const paginatedEntries = filteredEntries.slice(offset, offset + limit)
|
||||||
|
|
||||||
|
// Check if there are more items
|
||||||
|
const hasMore = offset + limit < totalCount
|
||||||
|
|
||||||
|
// Create a new map with the paginated entries
|
||||||
|
const items = new Map(paginatedEntries)
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
totalCount,
|
||||||
|
hasMore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the index
|
* Clear the index
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,37 @@ import { StatisticsData, StorageAdapter } from '../../coreTypes.js'
|
||||||
export abstract class BaseStorageAdapter implements StorageAdapter {
|
export abstract class BaseStorageAdapter implements StorageAdapter {
|
||||||
// Abstract methods that must be implemented by subclasses
|
// Abstract methods that must be implemented by subclasses
|
||||||
abstract init(): Promise<void>
|
abstract init(): Promise<void>
|
||||||
|
|
||||||
abstract saveNoun(noun: any): Promise<void>
|
abstract saveNoun(noun: any): Promise<void>
|
||||||
|
|
||||||
abstract getNoun(id: string): Promise<any | null>
|
abstract getNoun(id: string): Promise<any | null>
|
||||||
|
|
||||||
abstract getAllNouns(): Promise<any[]>
|
abstract getAllNouns(): Promise<any[]>
|
||||||
|
|
||||||
abstract getNounsByNounType(nounType: string): Promise<any[]>
|
abstract getNounsByNounType(nounType: string): Promise<any[]>
|
||||||
|
|
||||||
abstract deleteNoun(id: string): Promise<void>
|
abstract deleteNoun(id: string): Promise<void>
|
||||||
|
|
||||||
abstract saveVerb(verb: any): Promise<void>
|
abstract saveVerb(verb: any): Promise<void>
|
||||||
|
|
||||||
abstract getVerb(id: string): Promise<any | null>
|
abstract getVerb(id: string): Promise<any | null>
|
||||||
|
|
||||||
abstract getAllVerbs(): Promise<any[]>
|
abstract getAllVerbs(): Promise<any[]>
|
||||||
|
|
||||||
abstract getVerbsBySource(sourceId: string): Promise<any[]>
|
abstract getVerbsBySource(sourceId: string): Promise<any[]>
|
||||||
|
|
||||||
abstract getVerbsByTarget(targetId: string): Promise<any[]>
|
abstract getVerbsByTarget(targetId: string): Promise<any[]>
|
||||||
|
|
||||||
abstract getVerbsByType(type: string): Promise<any[]>
|
abstract getVerbsByType(type: string): Promise<any[]>
|
||||||
|
|
||||||
abstract deleteVerb(id: string): Promise<void>
|
abstract deleteVerb(id: string): Promise<void>
|
||||||
|
|
||||||
abstract saveMetadata(id: string, metadata: any): Promise<void>
|
abstract saveMetadata(id: string, metadata: any): Promise<void>
|
||||||
|
|
||||||
abstract getMetadata(id: string): Promise<any | null>
|
abstract getMetadata(id: string): Promise<any | null>
|
||||||
|
|
||||||
abstract clear(): Promise<void>
|
abstract clear(): Promise<void>
|
||||||
|
|
||||||
abstract getStorageStatus(): Promise<{
|
abstract getStorageStatus(): Promise<{
|
||||||
type: string
|
type: string
|
||||||
used: number
|
used: number
|
||||||
|
|
@ -33,26 +49,77 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
|
||||||
details?: Record<string, any>
|
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
|
// Statistics cache
|
||||||
protected statisticsCache: StatisticsData | null = null
|
protected statisticsCache: StatisticsData | null = null
|
||||||
|
|
||||||
// Batch update timer ID
|
// Batch update timer ID
|
||||||
protected statisticsBatchUpdateTimerId: NodeJS.Timeout | null = null
|
protected statisticsBatchUpdateTimerId: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
// Flag to indicate if statistics have been modified since last save
|
// Flag to indicate if statistics have been modified since last save
|
||||||
protected statisticsModified = false
|
protected statisticsModified = false
|
||||||
|
|
||||||
// Time of last statistics flush to storage
|
// Time of last statistics flush to storage
|
||||||
protected lastStatisticsFlushTime = 0
|
protected lastStatisticsFlushTime = 0
|
||||||
|
|
||||||
// Minimum time between statistics flushes (5 seconds)
|
// Minimum time between statistics flushes (5 seconds)
|
||||||
protected readonly MIN_FLUSH_INTERVAL_MS = 5000
|
protected readonly MIN_FLUSH_INTERVAL_MS = 5000
|
||||||
|
|
||||||
// Maximum time to wait before flushing statistics (30 seconds)
|
// Maximum time to wait before flushing statistics (30 seconds)
|
||||||
protected readonly MAX_FLUSH_DELAY_MS = 30000
|
protected readonly MAX_FLUSH_DELAY_MS = 30000
|
||||||
|
|
||||||
// Statistics-specific methods that must be implemented by subclasses
|
// 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>
|
protected abstract getStatisticsData(): Promise<StatisticsData | null>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -62,13 +129,13 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
|
||||||
async saveStatistics(statistics: StatisticsData): Promise<void> {
|
async saveStatistics(statistics: StatisticsData): Promise<void> {
|
||||||
// Update the cache with a deep copy to avoid reference issues
|
// Update the cache with a deep copy to avoid reference issues
|
||||||
this.statisticsCache = {
|
this.statisticsCache = {
|
||||||
nounCount: {...statistics.nounCount},
|
nounCount: { ...statistics.nounCount },
|
||||||
verbCount: {...statistics.verbCount},
|
verbCount: { ...statistics.verbCount },
|
||||||
metadataCount: {...statistics.metadataCount},
|
metadataCount: { ...statistics.metadataCount },
|
||||||
hnswIndexSize: statistics.hnswIndexSize,
|
hnswIndexSize: statistics.hnswIndexSize,
|
||||||
lastUpdated: statistics.lastUpdated
|
lastUpdated: statistics.lastUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule a batch update instead of saving immediately
|
// Schedule a batch update instead of saving immediately
|
||||||
this.scheduleBatchUpdate()
|
this.scheduleBatchUpdate()
|
||||||
}
|
}
|
||||||
|
|
@ -81,32 +148,32 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
|
||||||
// If we have cached statistics, return a deep copy
|
// If we have cached statistics, return a deep copy
|
||||||
if (this.statisticsCache) {
|
if (this.statisticsCache) {
|
||||||
return {
|
return {
|
||||||
nounCount: {...this.statisticsCache.nounCount},
|
nounCount: { ...this.statisticsCache.nounCount },
|
||||||
verbCount: {...this.statisticsCache.verbCount},
|
verbCount: { ...this.statisticsCache.verbCount },
|
||||||
metadataCount: {...this.statisticsCache.metadataCount},
|
metadataCount: { ...this.statisticsCache.metadataCount },
|
||||||
hnswIndexSize: this.statisticsCache.hnswIndexSize,
|
hnswIndexSize: this.statisticsCache.hnswIndexSize,
|
||||||
lastUpdated: this.statisticsCache.lastUpdated
|
lastUpdated: this.statisticsCache.lastUpdated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, get from storage
|
// Otherwise, get from storage
|
||||||
const statistics = await this.getStatisticsData()
|
const statistics = await this.getStatisticsData()
|
||||||
|
|
||||||
// If we found statistics, update the cache
|
// If we found statistics, update the cache
|
||||||
if (statistics) {
|
if (statistics) {
|
||||||
// Update the cache with a deep copy
|
// Update the cache with a deep copy
|
||||||
this.statisticsCache = {
|
this.statisticsCache = {
|
||||||
nounCount: {...statistics.nounCount},
|
nounCount: { ...statistics.nounCount },
|
||||||
verbCount: {...statistics.verbCount},
|
verbCount: { ...statistics.verbCount },
|
||||||
metadataCount: {...statistics.metadataCount},
|
metadataCount: { ...statistics.metadataCount },
|
||||||
hnswIndexSize: statistics.hnswIndexSize,
|
hnswIndexSize: statistics.hnswIndexSize,
|
||||||
lastUpdated: statistics.lastUpdated
|
lastUpdated: statistics.lastUpdated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return statistics
|
return statistics
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule a batch update of statistics
|
* Schedule a batch update of statistics
|
||||||
*/
|
*/
|
||||||
|
|
@ -124,9 +191,10 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
|
||||||
const timeSinceLastFlush = now - this.lastStatisticsFlushTime
|
const timeSinceLastFlush = now - this.lastStatisticsFlushTime
|
||||||
|
|
||||||
// If we've recently flushed, wait longer before the next flush
|
// If we've recently flushed, wait longer before the next flush
|
||||||
const delayMs = timeSinceLastFlush < this.MIN_FLUSH_INTERVAL_MS
|
const delayMs =
|
||||||
? this.MAX_FLUSH_DELAY_MS
|
timeSinceLastFlush < this.MIN_FLUSH_INTERVAL_MS
|
||||||
: this.MIN_FLUSH_INTERVAL_MS
|
? this.MAX_FLUSH_DELAY_MS
|
||||||
|
: this.MIN_FLUSH_INTERVAL_MS
|
||||||
|
|
||||||
// Schedule the batch update
|
// Schedule the batch update
|
||||||
this.statisticsBatchUpdateTimerId = setTimeout(() => {
|
this.statisticsBatchUpdateTimerId = setTimeout(() => {
|
||||||
|
|
@ -183,12 +251,12 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
|
||||||
if (!statistics) {
|
if (!statistics) {
|
||||||
statistics = this.createDefaultStatistics()
|
statistics = this.createDefaultStatistics()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cache
|
// Update the cache
|
||||||
this.statisticsCache = {
|
this.statisticsCache = {
|
||||||
nounCount: {...statistics.nounCount},
|
nounCount: { ...statistics.nounCount },
|
||||||
verbCount: {...statistics.verbCount},
|
verbCount: { ...statistics.verbCount },
|
||||||
metadataCount: {...statistics.metadataCount},
|
metadataCount: { ...statistics.metadataCount },
|
||||||
hnswIndexSize: statistics.hnswIndexSize,
|
hnswIndexSize: statistics.hnswIndexSize,
|
||||||
lastUpdated: statistics.lastUpdated
|
lastUpdated: statistics.lastUpdated
|
||||||
}
|
}
|
||||||
|
|
@ -229,12 +297,12 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
|
||||||
if (!statistics) {
|
if (!statistics) {
|
||||||
statistics = this.createDefaultStatistics()
|
statistics = this.createDefaultStatistics()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cache
|
// Update the cache
|
||||||
this.statisticsCache = {
|
this.statisticsCache = {
|
||||||
nounCount: {...statistics.nounCount},
|
nounCount: { ...statistics.nounCount },
|
||||||
verbCount: {...statistics.verbCount},
|
verbCount: { ...statistics.verbCount },
|
||||||
metadataCount: {...statistics.metadataCount},
|
metadataCount: { ...statistics.metadataCount },
|
||||||
hnswIndexSize: statistics.hnswIndexSize,
|
hnswIndexSize: statistics.hnswIndexSize,
|
||||||
lastUpdated: statistics.lastUpdated
|
lastUpdated: statistics.lastUpdated
|
||||||
}
|
}
|
||||||
|
|
@ -269,12 +337,12 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
|
||||||
if (!statistics) {
|
if (!statistics) {
|
||||||
statistics = this.createDefaultStatistics()
|
statistics = this.createDefaultStatistics()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cache
|
// Update the cache
|
||||||
this.statisticsCache = {
|
this.statisticsCache = {
|
||||||
nounCount: {...statistics.nounCount},
|
nounCount: { ...statistics.nounCount },
|
||||||
verbCount: {...statistics.verbCount},
|
verbCount: { ...statistics.verbCount },
|
||||||
metadataCount: {...statistics.metadataCount},
|
metadataCount: { ...statistics.metadataCount },
|
||||||
hnswIndexSize: statistics.hnswIndexSize,
|
hnswIndexSize: statistics.hnswIndexSize,
|
||||||
lastUpdated: statistics.lastUpdated
|
lastUpdated: statistics.lastUpdated
|
||||||
}
|
}
|
||||||
|
|
@ -317,4 +385,4 @@ export abstract class BaseStorageAdapter implements StorageAdapter {
|
||||||
lastUpdated: new Date().toISOString()
|
lastUpdated: new Date().toISOString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { GraphVerb, HNSWNoun, StatisticsData } from '../../coreTypes.js'
|
import { GraphVerb, HNSWNoun, StatisticsData } from '../../coreTypes.js'
|
||||||
import { BaseStorage, STATISTICS_KEY } from '../baseStorage.js'
|
import { BaseStorage, STATISTICS_KEY } from '../baseStorage.js'
|
||||||
|
import { PaginatedResult } from '../../types/paginationTypes.js'
|
||||||
|
|
||||||
// No type aliases needed - using the original types directly
|
// No type aliases needed - using the original types directly
|
||||||
|
|
||||||
|
|
@ -104,38 +105,124 @@ export class MemoryStorage extends BaseStorage {
|
||||||
return allNouns
|
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
|
* Get nouns by noun type
|
||||||
* @param nounType The noun type to filter by
|
* @param nounType The noun type to filter by
|
||||||
* @returns Promise that resolves to an array of nouns of the specified noun type
|
* @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[]> {
|
protected async getNounsByNounType_internal(nounType: string): Promise<HNSWNoun[]> {
|
||||||
const nouns: HNSWNoun[] = []
|
const result = await this.getNouns({
|
||||||
|
filter: {
|
||||||
// Iterate through all nouns and filter by noun type using metadata
|
nounType
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
return result.items
|
||||||
return nouns
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -271,28 +358,176 @@ export class MemoryStorage extends BaseStorage {
|
||||||
return allVerbs
|
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
|
* Get verbs by source
|
||||||
|
* @deprecated Use getVerbs() with filter.sourceId instead
|
||||||
*/
|
*/
|
||||||
protected async getVerbsBySource_internal(sourceId: string): Promise<GraphVerb[]> {
|
protected async getVerbsBySource_internal(sourceId: string): Promise<GraphVerb[]> {
|
||||||
const allVerbs = await this.getAllVerbs_internal()
|
const result = await this.getVerbs({
|
||||||
return allVerbs.filter((verb: GraphVerb) => (verb.sourceId || verb.source) === sourceId)
|
filter: {
|
||||||
|
sourceId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result.items
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get verbs by target
|
* Get verbs by target
|
||||||
|
* @deprecated Use getVerbs() with filter.targetId instead
|
||||||
*/
|
*/
|
||||||
protected async getVerbsByTarget_internal(targetId: string): Promise<GraphVerb[]> {
|
protected async getVerbsByTarget_internal(targetId: string): Promise<GraphVerb[]> {
|
||||||
const allVerbs = await this.getAllVerbs_internal()
|
const result = await this.getVerbs({
|
||||||
return allVerbs.filter((verb: GraphVerb) => (verb.targetId || verb.target) === targetId)
|
filter: {
|
||||||
|
targetId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result.items
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get verbs by type
|
* Get verbs by type
|
||||||
|
* @deprecated Use getVerbs() with filter.verbType instead
|
||||||
*/
|
*/
|
||||||
protected async getVerbsByType_internal(type: string): Promise<GraphVerb[]> {
|
protected async getVerbsByType_internal(type: string): Promise<GraphVerb[]> {
|
||||||
const allVerbs = await this.getAllVerbs_internal()
|
const result = await this.getVerbs({
|
||||||
return allVerbs.filter((verb: GraphVerb) => (verb.type || verb.verb) === type)
|
filter: {
|
||||||
|
verbType: type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result.items
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,393 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
return this.getVerbsByType_internal(type)
|
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
|
* 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
|
* @returns Promise that resolves to the statistics data or null if not found
|
||||||
*/
|
*/
|
||||||
protected abstract getStatisticsData(): Promise<StatisticsData | null>
|
protected abstract getStatisticsData(): Promise<StatisticsData | null>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
src/types/paginationTypes.ts
Normal file
130
src/types/paginationTypes.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
/**
|
||||||
|
* Types for pagination and filtering in data retrieval operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pagination options for data retrieval
|
||||||
|
*/
|
||||||
|
export interface PaginationOptions {
|
||||||
|
/**
|
||||||
|
* The number of items to skip (for offset-based pagination)
|
||||||
|
*/
|
||||||
|
offset?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of items to return
|
||||||
|
*/
|
||||||
|
limit?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token for cursor-based pagination (for continuing from a previous page)
|
||||||
|
*/
|
||||||
|
cursor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter options for noun retrieval
|
||||||
|
*/
|
||||||
|
export interface NounFilterOptions {
|
||||||
|
/**
|
||||||
|
* Filter by noun type
|
||||||
|
*/
|
||||||
|
nounType?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by service
|
||||||
|
*/
|
||||||
|
service?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by metadata fields (key-value pairs)
|
||||||
|
*/
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by creation date range
|
||||||
|
*/
|
||||||
|
createdAt?: {
|
||||||
|
from?: Date | number;
|
||||||
|
to?: Date | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by update date range
|
||||||
|
*/
|
||||||
|
updatedAt?: {
|
||||||
|
from?: Date | number;
|
||||||
|
to?: Date | number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter options for verb retrieval
|
||||||
|
*/
|
||||||
|
export interface VerbFilterOptions {
|
||||||
|
/**
|
||||||
|
* Filter by verb type
|
||||||
|
*/
|
||||||
|
verbType?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by source noun ID
|
||||||
|
*/
|
||||||
|
sourceId?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by target noun ID
|
||||||
|
*/
|
||||||
|
targetId?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by service
|
||||||
|
*/
|
||||||
|
service?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by metadata fields (key-value pairs)
|
||||||
|
*/
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by creation date range
|
||||||
|
*/
|
||||||
|
createdAt?: {
|
||||||
|
from?: Date | number;
|
||||||
|
to?: Date | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by update date range
|
||||||
|
*/
|
||||||
|
updatedAt?: {
|
||||||
|
from?: Date | number;
|
||||||
|
to?: Date | number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a paginated query
|
||||||
|
*/
|
||||||
|
export interface PaginatedResult<T> {
|
||||||
|
/**
|
||||||
|
* The items for the current page
|
||||||
|
*/
|
||||||
|
items: T[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of items matching the query (may be estimated)
|
||||||
|
*/
|
||||||
|
totalCount?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether there are more items available
|
||||||
|
*/
|
||||||
|
hasMore: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor for fetching the next page (for cursor-based pagination)
|
||||||
|
*/
|
||||||
|
nextCursor?: string;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue