feat: Integration Hub for external tool connectivity
- Add native config option: `new Brainy({ integrations: true })`
- OData integration for Excel Power Query and Power BI
- Google Sheets integration with Apps Script
- Server-Sent Events (SSE) for real-time streaming
- Webhooks for push notifications
- Zero-config with sensible defaults
- Full tree-shaking when disabled
This commit is contained in:
parent
24039e8a1a
commit
b5bc9000cf
28 changed files with 8186 additions and 1 deletions
427
tests/integrations/core.test.ts
Normal file
427
tests/integrations/core.test.ts
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
/**
|
||||
* Integration Hub - Core Tests
|
||||
*
|
||||
* Tests for EventBus, TabularExporter, and core functionality.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { EventBus } from '../../src/integrations/core/EventBus.js'
|
||||
import { TabularExporter } from '../../src/integrations/core/TabularExporter.js'
|
||||
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
||||
import type { Entity, Relation } from '../../src/types/brainy.types.js'
|
||||
|
||||
describe('EventBus', () => {
|
||||
let eventBus: EventBus
|
||||
|
||||
beforeEach(() => {
|
||||
eventBus = new EventBus()
|
||||
})
|
||||
|
||||
describe('subscribe and emit', () => {
|
||||
it('should emit events to subscribers', async () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
eventBus.subscribe({}, handler)
|
||||
|
||||
eventBus.emit({
|
||||
entityType: 'noun',
|
||||
operation: 'create',
|
||||
entityId: 'test-123'
|
||||
})
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityType: 'noun',
|
||||
operation: 'create',
|
||||
entityId: 'test-123'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should filter by entity type', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
eventBus.subscribe({ entityTypes: ['verb'] }, handler)
|
||||
|
||||
eventBus.emit({
|
||||
entityType: 'noun',
|
||||
operation: 'create',
|
||||
entityId: 'test-123'
|
||||
})
|
||||
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
|
||||
eventBus.emit({
|
||||
entityType: 'verb',
|
||||
operation: 'create',
|
||||
entityId: 'test-456'
|
||||
})
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should filter by operation', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
eventBus.subscribe({ operations: ['update', 'delete'] }, handler)
|
||||
|
||||
eventBus.emit({
|
||||
entityType: 'noun',
|
||||
operation: 'create',
|
||||
entityId: 'test-123'
|
||||
})
|
||||
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
|
||||
eventBus.emit({
|
||||
entityType: 'noun',
|
||||
operation: 'update',
|
||||
entityId: 'test-456'
|
||||
})
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should filter by noun type', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
eventBus.subscribe({ nounTypes: [NounType.Person] }, handler)
|
||||
|
||||
eventBus.emit({
|
||||
entityType: 'noun',
|
||||
operation: 'create',
|
||||
entityId: 'test-123',
|
||||
nounType: NounType.Document
|
||||
})
|
||||
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
|
||||
eventBus.emit({
|
||||
entityType: 'noun',
|
||||
operation: 'create',
|
||||
entityId: 'test-456',
|
||||
nounType: NounType.Person
|
||||
})
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsubscribe', () => {
|
||||
it('should stop receiving events after unsubscribe', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
const subscription = eventBus.subscribe({}, handler)
|
||||
|
||||
eventBus.emit({
|
||||
entityType: 'noun',
|
||||
operation: 'create',
|
||||
entityId: 'test-123'
|
||||
})
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1)
|
||||
|
||||
subscription.unsubscribe()
|
||||
|
||||
eventBus.emit({
|
||||
entityType: 'noun',
|
||||
operation: 'create',
|
||||
entityId: 'test-456'
|
||||
})
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1) // Still 1
|
||||
})
|
||||
})
|
||||
|
||||
describe('sequence IDs', () => {
|
||||
it('should increment sequence IDs', () => {
|
||||
const events: any[] = []
|
||||
eventBus.subscribe({}, (e) => events.push(e))
|
||||
|
||||
eventBus.emit({ entityType: 'noun', operation: 'create', entityId: '1' })
|
||||
eventBus.emit({ entityType: 'noun', operation: 'create', entityId: '2' })
|
||||
eventBus.emit({ entityType: 'noun', operation: 'create', entityId: '3' })
|
||||
|
||||
expect(events[0].sequenceId).toBe(1n)
|
||||
expect(events[1].sequenceId).toBe(2n)
|
||||
expect(events[2].sequenceId).toBe(3n)
|
||||
})
|
||||
|
||||
it('should return current sequence ID', () => {
|
||||
expect(eventBus.getCurrentSequenceId()).toBe(0n)
|
||||
|
||||
eventBus.emit({ entityType: 'noun', operation: 'create', entityId: '1' })
|
||||
expect(eventBus.getCurrentSequenceId()).toBe(1n)
|
||||
|
||||
eventBus.emit({ entityType: 'noun', operation: 'create', entityId: '2' })
|
||||
expect(eventBus.getCurrentSequenceId()).toBe(2n)
|
||||
})
|
||||
})
|
||||
|
||||
describe('event buffer', () => {
|
||||
it('should buffer events for replay', () => {
|
||||
eventBus.emit({ entityType: 'noun', operation: 'create', entityId: '1' })
|
||||
eventBus.emit({ entityType: 'noun', operation: 'create', entityId: '2' })
|
||||
eventBus.emit({ entityType: 'noun', operation: 'create', entityId: '3' })
|
||||
|
||||
const events = eventBus.getEventsSince(1n)
|
||||
|
||||
expect(events).toHaveLength(2)
|
||||
expect(events[0].entityId).toBe('2')
|
||||
expect(events[1].entityId).toBe('3')
|
||||
})
|
||||
})
|
||||
|
||||
describe('helper methods', () => {
|
||||
it('should emit noun events', () => {
|
||||
const handler = vi.fn()
|
||||
eventBus.subscribe({}, handler)
|
||||
|
||||
eventBus.emitNoun('create', 'entity-1', NounType.Person)
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityType: 'noun',
|
||||
operation: 'create',
|
||||
entityId: 'entity-1',
|
||||
nounType: NounType.Person
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should emit verb events', () => {
|
||||
const handler = vi.fn()
|
||||
eventBus.subscribe({}, handler)
|
||||
|
||||
eventBus.emitVerb('create', 'rel-1', VerbType.RelatedTo)
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityType: 'verb',
|
||||
operation: 'create',
|
||||
entityId: 'rel-1',
|
||||
verbType: VerbType.RelatedTo
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should emit VFS events', () => {
|
||||
const handler = vi.fn()
|
||||
eventBus.subscribe({}, handler)
|
||||
|
||||
eventBus.emitVFS('create', 'file-1')
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityType: 'vfs',
|
||||
operation: 'create',
|
||||
entityId: 'file-1'
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('TabularExporter', () => {
|
||||
let exporter: TabularExporter
|
||||
|
||||
const mockEntity: Entity = {
|
||||
id: 'entity-123',
|
||||
type: NounType.Person,
|
||||
createdAt: 1704067200000, // 2024-01-01
|
||||
updatedAt: 1704153600000, // 2024-01-02
|
||||
vector: new Float32Array([0.1, 0.2, 0.3]),
|
||||
confidence: 0.95,
|
||||
weight: 0.8,
|
||||
service: 'test-service',
|
||||
data: { content: 'Hello world' },
|
||||
metadata: {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
nested: { key: 'value' }
|
||||
}
|
||||
}
|
||||
|
||||
const mockRelation: Relation = {
|
||||
id: 'rel-123',
|
||||
from: 'entity-1',
|
||||
to: 'entity-2',
|
||||
type: VerbType.RelatedTo,
|
||||
weight: 0.9,
|
||||
confidence: 0.85,
|
||||
createdAt: 1704067200000,
|
||||
service: 'test-service',
|
||||
metadata: { reason: 'test' }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
exporter = new TabularExporter()
|
||||
})
|
||||
|
||||
describe('entitiesToRows', () => {
|
||||
it('should convert entity to row', () => {
|
||||
const rows = exporter.entitiesToRows([mockEntity])
|
||||
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0].Id).toBe('entity-123')
|
||||
expect(rows[0].Type).toBe('person')
|
||||
expect(rows[0].Confidence).toBe(0.95)
|
||||
expect(rows[0].Weight).toBe(0.8)
|
||||
expect(rows[0].Service).toBe('test-service')
|
||||
})
|
||||
|
||||
it('should flatten metadata by default', () => {
|
||||
const rows = exporter.entitiesToRows([mockEntity])
|
||||
|
||||
expect(rows[0].Metadata_name).toBe('John Doe')
|
||||
expect(rows[0].Metadata_email).toBe('john@example.com')
|
||||
})
|
||||
|
||||
it('should JSON stringify nested metadata', () => {
|
||||
const rows = exporter.entitiesToRows([mockEntity])
|
||||
|
||||
expect(rows[0].Metadata_nested).toBe('{"key":"value"}')
|
||||
})
|
||||
|
||||
it('should JSON stringify data field', () => {
|
||||
const rows = exporter.entitiesToRows([mockEntity])
|
||||
|
||||
expect(rows[0].Data).toBe('{"content":"Hello world"}')
|
||||
})
|
||||
|
||||
it('should format dates as ISO 8601', () => {
|
||||
const rows = exporter.entitiesToRows([mockEntity])
|
||||
|
||||
expect(rows[0].CreatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('configuration', () => {
|
||||
it('should not flatten metadata when disabled', () => {
|
||||
exporter = new TabularExporter({ flattenMetadata: false })
|
||||
const rows = exporter.entitiesToRows([mockEntity])
|
||||
|
||||
expect(rows[0].Metadata_name).toBeUndefined()
|
||||
expect(rows[0].Metadata).toBeDefined()
|
||||
})
|
||||
|
||||
it('should include vectors when enabled', () => {
|
||||
exporter = new TabularExporter({ includeVectors: true })
|
||||
const rows = exporter.entitiesToRows([mockEntity])
|
||||
|
||||
expect(rows[0].Vector).toBeDefined()
|
||||
})
|
||||
|
||||
it('should use custom metadata prefix', () => {
|
||||
exporter = new TabularExporter({ metadataPrefix: 'M_' })
|
||||
const rows = exporter.entitiesToRows([mockEntity])
|
||||
|
||||
expect(rows[0].M_name).toBe('John Doe')
|
||||
expect(rows[0].Metadata_name).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should format dates as unix timestamps', () => {
|
||||
exporter = new TabularExporter({ dateFormat: 'unix' })
|
||||
const rows = exporter.entitiesToRows([mockEntity])
|
||||
|
||||
expect(rows[0].CreatedAt).toBe('1704067200')
|
||||
})
|
||||
})
|
||||
|
||||
describe('relationsToRows', () => {
|
||||
it('should convert relation to row', () => {
|
||||
const rows = exporter.relationsToRows([mockRelation])
|
||||
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0].Id).toBe('rel-123')
|
||||
expect(rows[0].FromId).toBe('entity-1')
|
||||
expect(rows[0].ToId).toBe('entity-2')
|
||||
expect(rows[0].Type).toBe('relatedTo')
|
||||
expect(rows[0].Weight).toBe(0.9)
|
||||
expect(rows[0].Confidence).toBe(0.85)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toCSV', () => {
|
||||
it('should generate valid CSV', () => {
|
||||
const csv = exporter.toCSV([mockEntity])
|
||||
const lines = csv.split('\n')
|
||||
|
||||
expect(lines.length).toBeGreaterThan(1)
|
||||
expect(lines[0]).toContain('Id')
|
||||
expect(lines[0]).toContain('Type')
|
||||
expect(lines[1]).toContain('entity-123')
|
||||
})
|
||||
|
||||
it('should escape special characters', () => {
|
||||
const entityWithComma: Entity = {
|
||||
...mockEntity,
|
||||
metadata: { name: 'Doe, John' }
|
||||
}
|
||||
|
||||
const csv = exporter.toCSV([entityWithComma])
|
||||
|
||||
expect(csv).toContain('"Doe, John"')
|
||||
})
|
||||
|
||||
it('should escape quotes', () => {
|
||||
const entityWithQuotes: Entity = {
|
||||
...mockEntity,
|
||||
metadata: { name: 'John "Jack" Doe' }
|
||||
}
|
||||
|
||||
const csv = exporter.toCSV([entityWithQuotes])
|
||||
|
||||
expect(csv).toContain('""Jack""')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCSV', () => {
|
||||
it('should parse CSV back to entities', () => {
|
||||
const csv = exporter.toCSV([mockEntity])
|
||||
const entities = exporter.parseCSV(csv)
|
||||
|
||||
expect(entities).toHaveLength(1)
|
||||
expect(entities[0].id).toBe('entity-123')
|
||||
expect(entities[0].type).toBe('person')
|
||||
})
|
||||
})
|
||||
|
||||
describe('toOData', () => {
|
||||
it('should generate OData response', () => {
|
||||
const odata = exporter.toOData([mockEntity])
|
||||
|
||||
expect(odata).toHaveProperty('@odata.context')
|
||||
expect(odata).toHaveProperty('value')
|
||||
expect((odata as any).value).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should include count when provided', () => {
|
||||
const odata = exporter.toOData([mockEntity], { count: 100 })
|
||||
|
||||
expect((odata as any)['@odata.count']).toBe(100)
|
||||
})
|
||||
|
||||
it('should include nextLink when provided', () => {
|
||||
const odata = exporter.toOData([mockEntity], {
|
||||
nextLink: '/odata/Entities?$skip=100'
|
||||
})
|
||||
|
||||
expect((odata as any)['@odata.nextLink']).toBe('/odata/Entities?$skip=100')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSchema', () => {
|
||||
it('should infer schema from entities', () => {
|
||||
const schema = exporter.getSchema([mockEntity])
|
||||
|
||||
expect(schema).toContainEqual(
|
||||
expect.objectContaining({ name: 'Id', type: 'string' })
|
||||
)
|
||||
expect(schema).toContainEqual(
|
||||
expect.objectContaining({ name: 'Confidence', type: 'number' })
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
343
tests/integrations/odata.test.ts
Normal file
343
tests/integrations/odata.test.ts
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
/**
|
||||
* OData Integration Tests
|
||||
*
|
||||
* Tests for OData query parsing, EDMX generation, and filtering.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
parseODataQuery,
|
||||
parseFilter,
|
||||
parseOrderBy,
|
||||
parseSelect,
|
||||
applyFilter,
|
||||
applySelect,
|
||||
applyOrderBy,
|
||||
applyPagination
|
||||
} from '../../src/integrations/odata/ODataQueryParser.js'
|
||||
import {
|
||||
generateEdmx,
|
||||
generateMetadataJson,
|
||||
generateServiceDocument
|
||||
} from '../../src/integrations/odata/EdmxGenerator.js'
|
||||
|
||||
describe('OData Query Parser', () => {
|
||||
describe('parseODataQuery', () => {
|
||||
it('should parse $top', () => {
|
||||
const options = parseODataQuery('$top=10')
|
||||
expect(options.top).toBe(10)
|
||||
})
|
||||
|
||||
it('should parse $skip', () => {
|
||||
const options = parseODataQuery('$skip=20')
|
||||
expect(options.skip).toBe(20)
|
||||
})
|
||||
|
||||
it('should parse $select', () => {
|
||||
const options = parseODataQuery('$select=Id,Type,Name')
|
||||
expect(options.select).toEqual(['Id', 'Type', 'Name'])
|
||||
})
|
||||
|
||||
it('should parse $orderby', () => {
|
||||
const options = parseODataQuery('$orderby=CreatedAt desc')
|
||||
expect(options.orderBy).toEqual([
|
||||
{ field: 'CreatedAt', direction: 'desc' }
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse $count', () => {
|
||||
const options = parseODataQuery('$count=true')
|
||||
expect(options.count).toBe(true)
|
||||
})
|
||||
|
||||
it('should parse $filter', () => {
|
||||
const options = parseODataQuery("$filter=Type eq 'person'")
|
||||
expect(options.filter).toBe("Type eq 'person'")
|
||||
})
|
||||
|
||||
it('should parse $search', () => {
|
||||
const options = parseODataQuery('$search=machine learning')
|
||||
expect(options.search).toBe('machine learning')
|
||||
})
|
||||
|
||||
it('should parse multiple options', () => {
|
||||
const options = parseODataQuery(
|
||||
'$top=10&$skip=20&$orderby=Name asc&$count=true'
|
||||
)
|
||||
expect(options.top).toBe(10)
|
||||
expect(options.skip).toBe(20)
|
||||
expect(options.orderBy).toEqual([{ field: 'Name', direction: 'asc' }])
|
||||
expect(options.count).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseFilter', () => {
|
||||
it('should parse simple eq comparison', () => {
|
||||
const filter = parseFilter("Type eq 'person'")
|
||||
expect(filter).toEqual({
|
||||
field: 'Type',
|
||||
op: 'eq',
|
||||
value: 'person'
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse ne comparison', () => {
|
||||
const filter = parseFilter("Status ne 'deleted'")
|
||||
expect(filter).toEqual({
|
||||
field: 'Status',
|
||||
op: 'ne',
|
||||
value: 'deleted'
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse numeric comparisons', () => {
|
||||
const gtFilter = parseFilter('Weight gt 0.5')
|
||||
expect(gtFilter.op).toBe('gt')
|
||||
expect(gtFilter.value).toBe(0.5)
|
||||
|
||||
const ltFilter = parseFilter('Count lt 100')
|
||||
expect(ltFilter.op).toBe('lt')
|
||||
expect(ltFilter.value).toBe(100)
|
||||
})
|
||||
|
||||
it('should parse boolean values', () => {
|
||||
const filter = parseFilter('Active eq true')
|
||||
expect(filter.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should parse null values', () => {
|
||||
const filter = parseFilter('DeletedAt eq null')
|
||||
expect(filter.value).toBe(null)
|
||||
})
|
||||
|
||||
it('should parse and expressions', () => {
|
||||
const filter = parseFilter("Type eq 'person' and Status eq 'active'")
|
||||
expect(filter).toEqual({
|
||||
and: [
|
||||
{ field: 'Type', op: 'eq', value: 'person' },
|
||||
{ field: 'Status', op: 'eq', value: 'active' }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse or expressions', () => {
|
||||
const filter = parseFilter("Type eq 'person' or Type eq 'organization'")
|
||||
expect(filter).toEqual({
|
||||
or: [
|
||||
{ field: 'Type', op: 'eq', value: 'person' },
|
||||
{ field: 'Type', op: 'eq', value: 'organization' }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse contains function', () => {
|
||||
const filter = parseFilter("contains(Name, 'John')")
|
||||
expect(filter).toEqual({
|
||||
func: 'contains',
|
||||
args: [{ field: 'Name' }, { value: 'John' }]
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse startswith function', () => {
|
||||
const filter = parseFilter("startswith(Email, 'admin')")
|
||||
expect(filter).toEqual({
|
||||
func: 'startswith',
|
||||
args: [{ field: 'Email' }, { value: 'admin' }]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseOrderBy', () => {
|
||||
it('should parse single field', () => {
|
||||
const orderBy = parseOrderBy('Name')
|
||||
expect(orderBy).toEqual([{ field: 'Name', direction: 'asc' }])
|
||||
})
|
||||
|
||||
it('should parse with direction', () => {
|
||||
const orderBy = parseOrderBy('CreatedAt desc')
|
||||
expect(orderBy).toEqual([{ field: 'CreatedAt', direction: 'desc' }])
|
||||
})
|
||||
|
||||
it('should parse multiple fields', () => {
|
||||
const orderBy = parseOrderBy('Type asc, CreatedAt desc')
|
||||
expect(orderBy).toEqual([
|
||||
{ field: 'Type', direction: 'asc' },
|
||||
{ field: 'CreatedAt', direction: 'desc' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseSelect', () => {
|
||||
it('should parse comma-separated fields', () => {
|
||||
const select = parseSelect('Id, Type, Name')
|
||||
expect(select).toEqual(['Id', 'Type', 'Name'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyFilter', () => {
|
||||
const testData = [
|
||||
{ Id: '1', Type: 'person', Name: 'John Doe', Weight: 0.8 },
|
||||
{ Id: '2', Type: 'person', Name: 'Jane Smith', Weight: 0.6 },
|
||||
{ Id: '3', Type: 'organization', Name: 'Acme Corp', Weight: 0.9 },
|
||||
{ Id: '4', Type: 'document', Name: 'Report', Weight: 0.5 }
|
||||
]
|
||||
|
||||
it('should filter by eq', () => {
|
||||
const result = applyFilter(testData, "Type eq 'person'")
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.every((r) => r.Type === 'person')).toBe(true)
|
||||
})
|
||||
|
||||
it('should filter by gt', () => {
|
||||
const result = applyFilter(testData, 'Weight gt 0.7')
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.every((r) => r.Weight > 0.7)).toBe(true)
|
||||
})
|
||||
|
||||
it('should filter with contains', () => {
|
||||
const result = applyFilter(testData, "contains(Name, 'Doe')")
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].Name).toBe('John Doe')
|
||||
})
|
||||
|
||||
it('should filter with and', () => {
|
||||
const result = applyFilter(
|
||||
testData,
|
||||
"Type eq 'person' and Weight gt 0.7"
|
||||
)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].Name).toBe('John Doe')
|
||||
})
|
||||
})
|
||||
|
||||
describe('applySelect', () => {
|
||||
const testData = [
|
||||
{ Id: '1', Type: 'person', Name: 'John', Extra: 'data' }
|
||||
]
|
||||
|
||||
it('should select specified fields only', () => {
|
||||
const result = applySelect(testData, ['Id', 'Name'])
|
||||
expect(result[0]).toHaveProperty('Id')
|
||||
expect(result[0]).toHaveProperty('Name')
|
||||
expect(result[0]).not.toHaveProperty('Type')
|
||||
expect(result[0]).not.toHaveProperty('Extra')
|
||||
})
|
||||
|
||||
it('should return all fields if select is empty', () => {
|
||||
const result = applySelect(testData, [])
|
||||
expect(result[0]).toHaveProperty('Extra')
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyOrderBy', () => {
|
||||
const testData = [
|
||||
{ Name: 'Charlie', Weight: 0.5 },
|
||||
{ Name: 'Alice', Weight: 0.9 },
|
||||
{ Name: 'Bob', Weight: 0.7 }
|
||||
]
|
||||
|
||||
it('should sort ascending by default', () => {
|
||||
const result = applyOrderBy(testData, [
|
||||
{ field: 'Name', direction: 'asc' }
|
||||
])
|
||||
expect(result[0].Name).toBe('Alice')
|
||||
expect(result[1].Name).toBe('Bob')
|
||||
expect(result[2].Name).toBe('Charlie')
|
||||
})
|
||||
|
||||
it('should sort descending', () => {
|
||||
const result = applyOrderBy(testData, [
|
||||
{ field: 'Weight', direction: 'desc' }
|
||||
])
|
||||
expect(result[0].Weight).toBe(0.9)
|
||||
expect(result[2].Weight).toBe(0.5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyPagination', () => {
|
||||
const testData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
|
||||
it('should apply top (limit)', () => {
|
||||
const result = applyPagination(testData, 3)
|
||||
expect(result).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
it('should apply skip (offset)', () => {
|
||||
const result = applyPagination(testData, undefined, 5)
|
||||
expect(result).toEqual([6, 7, 8, 9, 10])
|
||||
})
|
||||
|
||||
it('should apply both top and skip', () => {
|
||||
const result = applyPagination(testData, 3, 2)
|
||||
expect(result).toEqual([3, 4, 5])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EDMX Generator', () => {
|
||||
describe('generateEdmx', () => {
|
||||
it('should generate valid XML', () => {
|
||||
const xml = generateEdmx()
|
||||
expect(xml).toContain('<?xml version="1.0"')
|
||||
expect(xml).toContain('<edmx:Edmx')
|
||||
expect(xml).toContain('EntityType Name="Entity"')
|
||||
})
|
||||
|
||||
it('should include entity properties', () => {
|
||||
const xml = generateEdmx()
|
||||
expect(xml).toContain('Property Name="Id"')
|
||||
expect(xml).toContain('Property Name="Type"')
|
||||
expect(xml).toContain('Property Name="CreatedAt"')
|
||||
})
|
||||
|
||||
it('should include NounType enum', () => {
|
||||
const xml = generateEdmx()
|
||||
expect(xml).toContain('EnumType Name="NounType"')
|
||||
expect(xml).toContain('Member Name="person"')
|
||||
})
|
||||
|
||||
it('should include relationships when enabled', () => {
|
||||
const xml = generateEdmx({ includeRelationships: true })
|
||||
expect(xml).toContain('EntityType Name="Relationship"')
|
||||
expect(xml).toContain('EnumType Name="VerbType"')
|
||||
})
|
||||
|
||||
it('should exclude relationships when disabled', () => {
|
||||
const xml = generateEdmx({ includeRelationships: false })
|
||||
expect(xml).not.toContain('EntityType Name="Relationship"')
|
||||
})
|
||||
|
||||
it('should use custom namespace', () => {
|
||||
const xml = generateEdmx({ namespace: 'MyApp' })
|
||||
expect(xml).toContain('Namespace="MyApp"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateMetadataJson', () => {
|
||||
it('should generate JSON metadata', () => {
|
||||
const metadata = generateMetadataJson()
|
||||
expect(metadata).toHaveProperty('$Version', '4.0')
|
||||
expect(metadata).toHaveProperty('Brainy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateServiceDocument', () => {
|
||||
it('should generate service document', () => {
|
||||
const doc = generateServiceDocument('http://localhost/odata')
|
||||
expect(doc).toHaveProperty('@odata.context')
|
||||
expect(doc).toHaveProperty('value')
|
||||
expect((doc as any).value).toContainEqual(
|
||||
expect.objectContaining({ name: 'Entities' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should include relationships when enabled', () => {
|
||||
const doc = generateServiceDocument('http://localhost/odata', {
|
||||
includeRelationships: true
|
||||
})
|
||||
expect((doc as any).value).toContainEqual(
|
||||
expect.objectContaining({ name: 'Relationships' })
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue