fix: resolve getRelations() empty array bug and add string ID shorthand
**Problem**: brain.getRelations() returned empty array when called without
parameters, making 524 imported relationships inaccessible for Workshop team.
**Root Cause**: Method only queried storage when `from` or `to` parameters
were provided. Without params, it returned empty array.
**Solution**:
- Add support for "get all" via storage.getVerbs() when no from/to provided
- Add string ID shorthand: getRelations(id) → getRelations({ from: id })
- Default limit: 100 (matching storage layer pattern)
- Production safety: warn for >10k queries without filters
- Fix broken improvedNeuralAPI.ts calls (getVerbsForNoun → getRelations)
- Fix property bugs: verb.target → verb.to, verb.verb → verb.type
**Testing**:
- 14 new integration tests covering all query patterns
- All critical tests passing (25/25)
- Backward compatible - no breaking changes
**Impact**: Resolves Workshop bug where imported relationships were invisible
This commit is contained in:
parent
0a9d7ffa65
commit
8d217f3b84
5 changed files with 605 additions and 28 deletions
|
|
@ -867,28 +867,91 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get relationships
|
||||
* Get relationships between entities
|
||||
*
|
||||
* Supports multiple query patterns:
|
||||
* - No parameters: Returns all relationships (paginated, default limit: 100)
|
||||
* - String ID: Returns relationships from that entity (shorthand for { from: id })
|
||||
* - Parameters object: Fine-grained filtering and pagination
|
||||
*
|
||||
* @param paramsOrId - Optional string ID or parameters object
|
||||
* @returns Promise resolving to array of relationships
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get all relationships (first 100)
|
||||
* const all = await brain.getRelations()
|
||||
*
|
||||
* // Get relationships from specific entity (shorthand syntax)
|
||||
* const fromEntity = await brain.getRelations(entityId)
|
||||
*
|
||||
* // Get relationships with filters
|
||||
* const filtered = await brain.getRelations({
|
||||
* type: VerbType.FriendOf,
|
||||
* limit: 50
|
||||
* })
|
||||
*
|
||||
* // Pagination
|
||||
* const page2 = await brain.getRelations({ offset: 100, limit: 100 })
|
||||
* ```
|
||||
*
|
||||
* @since v4.1.3 - Fixed bug where calling without parameters returned empty array
|
||||
* @since v4.1.3 - Added string ID shorthand syntax: getRelations(id)
|
||||
*/
|
||||
async getRelations(
|
||||
params: GetRelationsParams = {}
|
||||
paramsOrId?: string | GetRelationsParams
|
||||
): Promise<Relation<T>[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const relations: Relation<T>[] = []
|
||||
// Handle string ID shorthand: getRelations(id) -> getRelations({ from: id })
|
||||
const params = typeof paramsOrId === 'string'
|
||||
? { from: paramsOrId }
|
||||
: (paramsOrId || {})
|
||||
|
||||
const limit = params.limit || 100
|
||||
const offset = params.offset || 0
|
||||
|
||||
let relations: Relation<T>[] = []
|
||||
|
||||
// Case 1: Filter by source
|
||||
if (params.from) {
|
||||
const verbs = await this.storage.getVerbsBySource(params.from)
|
||||
relations.push(...this.verbsToRelations(verbs))
|
||||
relations.push(...this.verbsToRelations(verbs as any))
|
||||
}
|
||||
|
||||
if (params.to) {
|
||||
// Case 2: Filter by target
|
||||
else if (params.to) {
|
||||
const verbs = await this.storage.getVerbsByTarget(params.to)
|
||||
relations.push(...this.verbsToRelations(verbs))
|
||||
relations.push(...this.verbsToRelations(verbs as any))
|
||||
}
|
||||
// Case 3: Get ALL relationships (NEW - fixes v4.1.2 bug)
|
||||
else {
|
||||
// Production safety: warn for large unfiltered queries
|
||||
if (!params.type && limit > 10000) {
|
||||
console.warn(
|
||||
`[Brainy] getRelations(): Fetching ${limit} relationships without filters. ` +
|
||||
`Consider adding 'type' filter or reducing 'limit' for better performance.`
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch from storage using pagination
|
||||
const result = await this.storage.getVerbs({
|
||||
pagination: {
|
||||
limit: limit + offset, // Fetch enough for offset + limit
|
||||
offset: 0,
|
||||
cursor: params.cursor
|
||||
},
|
||||
filter: params.type
|
||||
? { verbType: Array.isArray(params.type) ? params.type : [params.type] as any }
|
||||
: undefined
|
||||
})
|
||||
|
||||
relations = this.verbsToRelations(result.items as any)
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
// Filter by type (only if not already filtered at storage level)
|
||||
let filtered = relations
|
||||
if (params.type) {
|
||||
if (params.type && (params.from || params.to)) {
|
||||
// Type filter only needed for from/to queries
|
||||
const types = Array.isArray(params.type) ? params.type : [params.type]
|
||||
filtered = relations.filter((r) => types.includes(r.type))
|
||||
}
|
||||
|
|
@ -898,9 +961,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
filtered = filtered.filter((r) => r.service === params.service)
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
const limit = params.limit || 100
|
||||
const offset = params.offset || 0
|
||||
// Apply pagination (for from/to queries, or trim excess from storage query)
|
||||
return filtered.slice(offset, offset + limit)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1617,18 +1617,18 @@ export class ImprovedNeuralAPI {
|
|||
// Get all verbs connecting the items
|
||||
for (const sourceId of itemIds) {
|
||||
const sourceVerbs = await this.brain.getRelations(sourceId)
|
||||
|
||||
|
||||
for (const verb of sourceVerbs) {
|
||||
const targetId = verb.target
|
||||
|
||||
const targetId = verb.to
|
||||
|
||||
if (nodes.has(targetId) && sourceId !== targetId) {
|
||||
// Initialize edge map if needed
|
||||
if (!edges.has(sourceId)) {
|
||||
edges.set(sourceId, new Map())
|
||||
}
|
||||
|
||||
|
||||
// Calculate edge weight from verb type and metadata
|
||||
const verbType = verb.verb
|
||||
const verbType = verb.type
|
||||
const baseWeight = (relationshipWeights as Record<string, number>)[verbType] || 0.5
|
||||
const confidenceWeight = verb.confidence || 1.0
|
||||
const weight = baseWeight * confidenceWeight
|
||||
|
|
@ -3437,7 +3437,7 @@ export class ImprovedNeuralAPI {
|
|||
const sampleSize = Math.min(50, itemIds.length)
|
||||
for (let i = 0; i < sampleSize; i++) {
|
||||
try {
|
||||
const verbs = await this.brain.getVerbsForNoun(itemIds[i])
|
||||
const verbs = await this.brain.getRelations({ from: itemIds[i] })
|
||||
connectionCount += verbs.length
|
||||
} catch (error) {
|
||||
// Skip items that can't be processed
|
||||
|
|
@ -3507,10 +3507,10 @@ export class ImprovedNeuralAPI {
|
|||
if (fromType !== toType) {
|
||||
for (const fromItem of fromItems.slice(0, 10)) { // Sample to avoid N^2
|
||||
try {
|
||||
const verbs = await this.brain.getVerbsForNoun(fromItem.id)
|
||||
|
||||
const verbs = await this.brain.getRelations({ from: fromItem.id })
|
||||
|
||||
for (const verb of verbs) {
|
||||
const toItem = toItems.find(item => item.id === verb.target)
|
||||
const toItem = toItems.find(item => item.id === verb.to)
|
||||
if (toItem) {
|
||||
connections.push({
|
||||
from: fromItem.id,
|
||||
|
|
|
|||
|
|
@ -217,15 +217,90 @@ export interface SimilarParams<T = any> {
|
|||
|
||||
/**
|
||||
* Parameters for getting relationships
|
||||
*
|
||||
* All parameters are optional. When called without parameters, returns all relationships
|
||||
* with pagination (default limit: 100).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get all relationships (default limit: 100)
|
||||
* const all = await brain.getRelations()
|
||||
*
|
||||
* // Get relationships from a specific entity (string shorthand)
|
||||
* const fromEntity = await brain.getRelations(entityId)
|
||||
*
|
||||
* // Equivalent to:
|
||||
* const fromEntity2 = await brain.getRelations({ from: entityId })
|
||||
*
|
||||
* // Get relationships to a specific entity
|
||||
* const toEntity = await brain.getRelations({ to: entityId })
|
||||
*
|
||||
* // Filter by relationship type
|
||||
* const friends = await brain.getRelations({ type: VerbType.FriendOf })
|
||||
*
|
||||
* // Pagination
|
||||
* const page2 = await brain.getRelations({ offset: 100, limit: 50 })
|
||||
*
|
||||
* // Combined filters
|
||||
* const filtered = await brain.getRelations({
|
||||
* from: entityId,
|
||||
* type: VerbType.WorksWith,
|
||||
* limit: 20
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @since v4.1.3 - Fixed bug where calling without parameters returned empty array
|
||||
* @since v4.1.3 - Added string ID shorthand syntax
|
||||
*/
|
||||
export interface GetRelationsParams {
|
||||
from?: string // Source entity
|
||||
to?: string // Target entity
|
||||
type?: VerbType | VerbType[] // Relationship types
|
||||
limit?: number // Max results
|
||||
offset?: number // Pagination
|
||||
cursor?: string // Cursor pagination
|
||||
service?: string // Multi-tenancy
|
||||
/**
|
||||
* Filter by source entity ID
|
||||
*
|
||||
* Returns all relationships originating from this entity.
|
||||
*/
|
||||
from?: string
|
||||
|
||||
/**
|
||||
* Filter by target entity ID
|
||||
*
|
||||
* Returns all relationships pointing to this entity.
|
||||
*/
|
||||
to?: string
|
||||
|
||||
/**
|
||||
* Filter by relationship type(s)
|
||||
*
|
||||
* Can be a single VerbType or array of VerbTypes.
|
||||
*/
|
||||
type?: VerbType | VerbType[]
|
||||
|
||||
/**
|
||||
* Maximum number of results to return
|
||||
*
|
||||
* @default 100
|
||||
*/
|
||||
limit?: number
|
||||
|
||||
/**
|
||||
* Number of results to skip (offset-based pagination)
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
offset?: number
|
||||
|
||||
/**
|
||||
* Cursor for cursor-based pagination
|
||||
*
|
||||
* More efficient than offset for large result sets.
|
||||
*/
|
||||
cursor?: string
|
||||
|
||||
/**
|
||||
* Filter by service (multi-tenancy)
|
||||
*
|
||||
* Only return relationships belonging to this service.
|
||||
*/
|
||||
service?: string
|
||||
}
|
||||
|
||||
// ============= Batch Operations =============
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue