/** * Unit tests for AddParams.upsert — create-or-update in a single add() call. * * Covers the live add() path and the addMany batch flag. Verifies that upsert * inserts on a fresh id, merges (not overwrites) into an existing id while * preserving createdAt and bumping _rev, is mutually exclusive with ifAbsent, * and is a plain insert when no id is supplied. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { Brainy } from '../../../src/brainy' import { createTestConfig } from '../../helpers/test-factory' describe('AddParams.upsert', () => { let brain: Brainy beforeEach(async () => { brain = new Brainy(createTestConfig()) await brain.init() }) afterEach(async () => { await brain.close() }) it('inserts a new entity when the id does not exist (_rev 1)', async () => { // 8.0 normalizes a natural string key to a stable canonical UUID; the entity is // still reachable by the natural key via get()'s id resolution. const id = await brain.add({ id: 'customer-1', type: 'thing', data: 'Acme Corp', metadata: { tier: 'silver' }, upsert: true }) expect(typeof id).toBe('string') const entity = await brain.get('customer-1') expect(entity).not.toBeNull() expect(entity!.type).toBe('thing') expect(entity!.metadata.tier).toBe('silver') expect(entity!._rev).toBe(1) }) it('merges into an existing entity, preserves createdAt, bumps _rev, applies changed data', async () => { // Seed the entity. await brain.add({ id: 'invoice-9', type: 'thing', data: 'Original line items', metadata: { status: 'draft', amount: 100 } }) const before = await brain.get('invoice-9') expect(before).not.toBeNull() const originalCreatedAt = before!.createdAt expect(before!._rev).toBe(1) // Upsert over the existing id with a partial metadata patch + new data. await brain.add({ id: 'invoice-9', type: 'thing', data: 'Revised line items', metadata: { status: 'sent' }, upsert: true }) const after = await brain.get('invoice-9') expect(after).not.toBeNull() // Metadata MERGED, not replaced — the untouched 'amount' field survives. expect(after!.metadata.status).toBe('sent') expect(after!.metadata.amount).toBe(100) // Changed data applied. expect(after!.data).toBe('Revised line items') // _rev bumped, createdAt preserved. expect(after!._rev).toBe(2) expect(after!.createdAt).toBe(originalCreatedAt) }) it('re-embeds when data changes on upsert', async () => { await brain.add({ id: 'doc-vec', type: 'document', data: 'cats and dogs and small furry animals' }) const before = await brain.get('doc-vec', { includeVectors: true }) expect(before!.vector.length).toBeGreaterThan(0) await brain.add({ id: 'doc-vec', type: 'document', data: 'quarterly financial revenue projections spreadsheet', upsert: true }) const after = await brain.get('doc-vec', { includeVectors: true }) expect(after!.vector.length).toBe(before!.vector.length) // A genuinely different document re-embeds to a different vector. expect(after!.vector).not.toEqual(before!.vector) }) it('throws when both ifAbsent and upsert are true', async () => { await expect( brain.add({ id: 'conflict-1', type: 'thing', data: 'x', ifAbsent: true, upsert: true }) ).rejects.toThrow(/mutually exclusive/) }) it('behaves like a plain insert when no id is supplied', async () => { const id = await brain.add({ type: 'thing', data: 'no id supplied', metadata: { k: 'v' }, upsert: true }) expect(typeof id).toBe('string') const entity = await brain.get(id) expect(entity).not.toBeNull() expect(entity!._rev).toBe(1) expect(entity!.metadata.k).toBe('v') }) it('merges an existing item via the addMany batch upsert flag', async () => { // Seed an item that the batch will later upsert into. await brain.add({ id: 'order-1', type: 'thing', data: 'first', metadata: { region: 'west', count: 1 } }) const seeded = await brain.get('order-1') const seededCreatedAt = seeded!.createdAt const result = await brain.addMany({ upsert: true, items: [ { id: 'order-1', type: 'thing', data: 'updated', metadata: { count: 2 } }, { id: 'order-2', type: 'thing', data: 'brand new', metadata: { region: 'east' } } ] }) // Both items resolved (returned ids are the canonical UUIDs the natural keys map to). expect(result.failed).toHaveLength(0) expect(result.successful).toHaveLength(2) // Existing item merged (region survived), _rev bumped, createdAt preserved. const merged = await brain.get('order-1') expect(merged!.metadata.region).toBe('west') expect(merged!.metadata.count).toBe(2) expect(merged!.data).toBe('updated') expect(merged!._rev).toBe(2) expect(merged!.createdAt).toBe(seededCreatedAt) // New item inserted fresh. const fresh = await brain.get('order-2') expect(fresh!._rev).toBe(1) expect(fresh!.metadata.region).toBe('east') }) it('upserts inside a transact batch — existing id merges, new id inserts', async () => { await brain.add({ id: 'acct-1', type: 'thing', data: 'seed', metadata: { plan: 'free', seats: 1 } }) const seeded = await brain.get('acct-1') const seededCreatedAt = seeded!.createdAt await brain.transact([ { op: 'add', id: 'acct-1', type: 'thing', data: 'seed', metadata: { plan: 'pro' }, upsert: true }, { op: 'add', id: 'acct-2', type: 'thing', data: 'new account', metadata: { plan: 'free' }, upsert: true } ]) const merged = await brain.get('acct-1') expect(merged!.metadata.plan).toBe('pro') expect(merged!.metadata.seats).toBe(1) // merged, not overwritten expect(merged!._rev).toBe(2) expect(merged!.createdAt).toBe(seededCreatedAt) const fresh = await brain.get('acct-2') expect(fresh!._rev).toBe(1) expect(fresh!.metadata.plan).toBe('free') }) })