/** * Tests for JSON object/array support in a datom's `vl` field. * * Covers: schema validation, deep-equality dedup, value-index keying, * matcher semantics (deep-eq literal match, anyOf membership, predicate * for nested access) and the fail-loud behaviour for bare-array patterns. */ import { describe, expect, it } from 'vitest' import { isValidApplog } from './datom-types.ts' import { matchPartStatic, valueEq, valueKey } from './applog-utils.ts' import { anyOf } from '../query/matchers.ts' import { finalizeApplogForInsert } from './applog-helpers.ts' import { applogsByAttrValue } from '../thread/indexes.ts' import { ThreadInMemory } from '../thread/writeable.ts' import type { Applog, ApplogForInsert } from './datom-types.ts' let tsCounter = 0 function makeLog(spec: Pick & Partial): Applog { tsCounter++ return finalizeApplogForInsert({ ts: new Date(1700000000000 + tsCounter * 1000).toISOString(), pv: null, ag: 'testAgent', ...spec, } as ApplogForInsert, {}) } describe('object/array applog values', () => { it('schema accepts nested objects and arrays as vl', () => { const obj = makeLog({ en: 'e1', at: 'config', vl: { theme: 'dark', nested: { n: [1, 2, 3] } } }) const arr = makeLog({ en: 'e1', at: 'tags', vl: ['a', 'b', { x: 1 }] }) expect(isValidApplog(obj)).toBe(true) expect(isValidApplog(arr)).toBe(true) // primitives still valid expect(isValidApplog(makeLog({ en: 'e1', at: 'name', vl: 'hi' }))).toBe(true) expect(isValidApplog(makeLog({ en: 'e1', at: 'n', vl: null }))).toBe(true) }) it('valueEq compares object/array values by structure', () => { expect(valueEq({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true) expect(valueEq([1, 2, 3], [1, 2, 3])).toBe(true) expect(valueEq({ a: 1 }, { a: 2 })).toBe(false) expect(valueEq('x', 'x')).toBe(true) }) it('valueKey canonicalizes objects (key-order independent) without colliding with strings', () => { expect(valueKey({ a: 1, b: 2 })).toBe(valueKey({ b: 2, a: 1 })) // a literal string equal to an object's serialization must NOT collide expect(valueKey('{"a":1}')).not.toBe(valueKey({ a: 1 })) // primitives pass through as their own value expect(valueKey('hi')).toBe('hi') expect(valueKey(42)).toBe(42) expect(valueKey(null)).toBe(null) }) it('hasApplogWithDiffTs matches structurally-equal object values (deep, key-order independent)', () => { const existing = makeLog({ en: 'e1', at: 'config', vl: { a: 1, b: 2 } }) const thread = ThreadInMemory.fromArray([existing], 'dedup') // same en/at/ag, deep-equal vl with different key order → recognised as the same datom expect(thread.hasApplogWithDiffTs({ en: 'e1', at: 'config', vl: { b: 2, a: 1 }, ag: 'testAgent' } as any)).toBeTruthy() // a structurally different value is NOT matched expect(thread.hasApplogWithDiffTs({ en: 'e1', at: 'config', vl: { a: 9 }, ag: 'testAgent' } as any)).toBeFalsy() }) it('applogsByAttrValue groups structurally-equal object values into one bucket', () => { const logs = [ makeLog({ en: 'e1', at: 'config', vl: { a: 1, b: 2 } }), makeLog({ en: 'e2', at: 'config', vl: { b: 2, a: 1 } }), makeLog({ en: 'e3', at: 'config', vl: { a: 9 } }), ] const thread = ThreadInMemory.fromArray(logs, 'idx') const index = applogsByAttrValue(thread, 'config').value expect(index.get(valueKey({ a: 1, b: 2 }))).toHaveLength(2) expect(index.get(valueKey({ a: 9 }))).toHaveLength(1) }) it('matchPartStatic matches a literal object value by deep equality', () => { expect(matchPartStatic('vl', { a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true) expect(matchPartStatic('vl', { a: 1 }, { a: 2 })).toBe(false) }) it('matching a literal array value requires a predicate (a bare array throws)', () => { // bare arrays are reserved/rejected, so an array value is matched via a predicate expect(matchPartStatic('vl', (v: any) => valueEq(v, [1, 2, 3]), [1, 2, 3])).toBe(true) expect(() => matchPartStatic('vl', [1, 2, 3] as any, [1, 2, 3])).toThrow(/anyOf/) }) it('anyOf(...) provides set-membership and matches via matchPartStatic', () => { expect(matchPartStatic('at', anyOf('a', 'b', 'c'), 'b')).toBe(true) expect(matchPartStatic('at', anyOf('a', 'b'), 'z')).toBe(false) }) it('a predicate matcher can reach into nested object values', () => { const isDark = (v: any) => v?.theme === 'dark' expect(matchPartStatic('vl', isDark, { theme: 'dark' })).toBe(true) expect(matchPartStatic('vl', isDark, { theme: 'light' })).toBe(false) }) it('a bare array pattern fails loudly rather than matching ambiguously', () => { expect(() => matchPartStatic('at', ['a', 'b'] as any, 'a')).toThrow(/anyOf/) }) it('anyOf rejects object/array members (would silently never match)', () => { expect(() => anyOf({ a: 1 } as any)).toThrow(/object\/array/) }) })