import { expect } from 'chai' ; import { ulid } from 'ulid' ; import { Query } from '../middleware/mongo' ; import { NewDexie } from '../utils' ; import { shouldHaveThrown } from '../utils/spec.utils' ; import { Collection } from './collection' ; const databaseName = 'collection' ; describe ( 'ThreadDB' , function ( ) { context ( 'collection' , function ( ) { const db = NewDexie ( databaseName ) ; after ( async function ( ) { db . close ( ) ; await db . delete ( ) ; } ) ; describe ( 'workflows' , async function ( ) { before ( async function ( ) { db . version ( 1 ) . stores ( { things : '++_id,thing' } ) ; } ) ; it ( 'should handle a normal db workflow' , async function ( ) { interface Info { _id ? : string ; other ? : number ; thing : string ; } const Thing = new Collection < Info > ( db . table ( 'things' ) ) ; const data = { _id : ulid ( ) , thing : 'one' } ; const thing1 = data ; expect ( thing1 . thing ) . to . equal ( 'one' ) ; thing1 . other = 1 ; expect ( thing1 . other ) . to . equal ( 1 ) ; expect ( await Thing . find ( { } ) . count ( ) ) . to . equal ( 0 ) ; await Thing . save ( thing1 ) ; expect ( await Thing . find ( { } ) . count ( ) ) . to . equal ( 1 ) ; await Thing . save ( data ) ; try { await Thing . insert ( data ) ; throw shouldHaveThrown ; } catch ( err ) { expect ( err ) . to . not . equal ( shouldHaveThrown ) ; } await Thing . insert ( { other : - 1 , thing : 'five' } , { other : 2 , thing : 'two' } , { other : 3 , thing : 'three' } , { other : 4 , thing : 'four' } ) ; const all = await Thing . find ( { $or : [ { other : { $gt : 1 } } , { thing : { $eq : 'one' } } ] , } ) . sortBy ( '_id' ) ; const last = all [ 0 ] ; expect ( last ) . to . have . haveOwnProperty ( 'other' , 1 ) ; } ) ; } ) ; describe ( 'units' , ( ) => { type Person = { name : string ; age : number ; } ; const defaultPerson = Object . freeze ( { name : 'Lucas' , age : 7 , } ) ; const copyPerson = ( person = defaultPerson , _id ? ) : Person & { _id ? : string ; } => { const out = { ... person } ; if ( _id ) out . _id = _id ; return out ; } ; const setupCollection = ( ) => { return new Collection < Person > ( db . table ( 'Person' ) ) ; } ; before ( async function ( ) { db . close ( ) ; db . version ( 2 ) . stores ( { Person : '++_id,name,age' } ) ; await db . open ( ) ; } ) ; beforeEach ( ( ) => { db . tables . forEach ( ( table ) => table . clear ( ) ) ; } ) ; describe ( 'top-level instance' , ( ) => { it ( 'should have a name property' , function ( ) { const Person = setupCollection ( ) ; expect ( Person . name ) . to . equal ( 'Person' ) ; } ) ; it ( 'should handle multiple write operations' , async function ( ) { const Person = setupCollection ( ) ; await Person . insert ( copyPerson ( ) , copyPerson ( ) , copyPerson ( ) ) ; const person = copyPerson ( ) ; const [ id ] = await Person . insert ( person ) ; await Person . delete ( id ) ; expect ( await Person . find ( { } ) . count ( ) ) . to . equal ( 3 ) ; } ) ; } ) ; describe ( 'creating entities' , ( ) => { it ( 'should create single entity from data' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; expect ( person ) . to . not . have . ownProperty ( '_id' ) ; const entity = Person . create ( person ) ; expect ( entity ) . to . have . ownProperty ( '_id' ) ; expect ( await entity . exists ( ) ) . to . equal ( false ) ; expect ( await Person . has ( entity . _id ) ) . to . equal ( false ) ; await entity . save ( ) ; expect ( await entity . exists ( ) ) . to . equal ( true ) ; expect ( await Person . has ( entity . _id ) ) . to . equal ( true ) ; } ) ; it ( 'should create entities with ulid ids by default' , async function ( ) { const Person = setupCollection ( ) ; const person1 = copyPerson ( ) ; const [ id ] = await Person . insert ( person1 ) ; expect ( person1 . _id ) . to . equal ( id ) ; const obj = await Person . findById ( id ) ; expect ( obj ) . to . have . ownProperty ( '_id' ) ; expect ( obj ?. _id ) . to . equal ( id ) ; expect ( obj ?. _id ) . to . have . length ( 26 ) ; } ) ; it ( 'should be able to override ulid id' , async function ( ) { const Person = setupCollection ( ) ; const person1 = copyPerson ( ) ; person1 . _id = 'override' ; const [ id ] = await Person . insert ( person1 ) ; expect ( person1 . _id ) . to . equal ( 'override' ) ; const obj = await Person . findById ( id ) ; expect ( obj ) . to . have . ownProperty ( '_id' ) ; expect ( obj ?. _id ) . to . equal ( id ) ; } ) ; it ( 'should create a single entity (w/ type checking) at a time' , async function ( ) { const Person = setupCollection ( ) ; const person1 = copyPerson ( ) ; const [ id ] = await Person . insert ( person1 ) ; const exists = await Person . has ( id ) ; const obj = await Person . findById ( id ) ; expect ( exists ) . to . be . true ; expect ( obj ) . to . have . ownProperty ( '_id' , id ) ; expect ( obj ?. save ) . to . not . be . undefined ; const person2 = copyPerson ( undefined , ulid ( ) ) ; let has = await Person . has ( person2 . _id ! ) ; expect ( has ) . to . equal ( false ) ; await Person . insert ( person2 ) ; has = await Person . has ( person2 . _id ! ) ; expect ( has ) . to . equal ( true ) ; } ) ; it ( 'should create multiple entities (variadic arguments w/ type checking) at once' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; const [ id ] = await Person . insert ( person , copyPerson ( ) , copyPerson ( ) ) ; expect ( await Person . find ( { } ) . count ( ) ) . to . equal ( 3 ) ; expect ( await Person . has ( id ) ) . to . be . true ; } ) ; it ( 'should create an entity with a predefined id' , async function ( ) { const Person = setupCollection ( ) ; const _id = ulid ( ) ; const person = { _id , name : 'Hans' , age : 12 } ; const [ id_ ] = await Person . insert ( person ) ; expect ( id_ ) . to . equal ( _id ) ; } ) ; it ( 'should not overwrite an existing entity' , async function ( ) { const Person = setupCollection ( ) ; const _id = ulid ( ) ; try { await Person . insert ( { _id , name : 'Hans' , age : 12 } as Person ) ; const person = { _id , name : 'Hans' , age : 12 } ; await Person . insert ( person ) ; throw shouldHaveThrown ; } catch ( err ) { expect ( err ) . to . not . equal ( shouldHaveThrown ) ; } } ) ; } ) ; describe ( 'creating transactions' , ( ) => { it ( 'should create a readonly transaction' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; const [ id ] = await Person . insert ( person ) ; Person . readTransaction ( async function ( ) { expect ( await Person . has ( id ) ) . to . be . true ; } ) ; } ) ; it ( 'should create a write transaction' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; await Person . writeTransaction ( async function ( ) { const [ id ] = await Person . insert ( person ) ; expect ( await Person . has ( id ) ) . to . be . true ; } ) ; } ) ; } ) ; describe ( 'checking for entities' , ( ) => { it ( 'should test for existing entity' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; const [ id1 ] = await Person . insert ( person ) ; expect ( await Person . has ( id1 ) ) . to . be . true ; expect ( await Person . has ( 'blah' ) ) . to . be . false ; const person2 = copyPerson ( ) ; const [ id2 ] = await Person . insert ( person2 ) ; expect ( await Person . has ( id2 ) ) . to . be . true ; await Person . delete ( id2 ) ; expect ( await Person . has ( id2 ) ) . to . be . false ; const [ id3 ] = await Person . insert ( person2 ) ; const personInstance = await Person . findById ( id3 ) ; if ( personInstance ) { expect ( await personInstance . exists ( ) ) . to . equal ( true ) ; await personInstance . remove ( ) ; expect ( await personInstance . exists ( ) ) . to . equal ( false ) ; } else { throw new Error ( 'should not be undefined' ) ; } } ) ; it ( 'should test for multiple entities' , async function ( ) { const Person = setupCollection ( ) ; const persons = [ copyPerson ( ) , copyPerson ( ) , copyPerson ( ) ] ; const ids = await Person . insert ( ... persons ) ; expect ( await Promise . all ( ids . map ( ( id ) => Person . has ( id ) ) ) ) . to . deep . equal ( [ true , true , true ] ) ; expect ( await Promise . all ( [ 'foo' , 'bar' , 'baz' ] . map ( ( p ) => Person . has ( p ) ) ) ) . to . deep . equal ( [ false , false , false ] ) ; } ) ; } ) ; describe ( 'returning entities' , ( ) => { it ( 'should get existing entity' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; const [ id ] = await Person . insert ( person ) ; let found = await Person . findById ( id ) ; expect ( found ) . to . deep . equal ( person ) ; found = await Person . findById ( 'blah' ) ; expect ( found ) . to . be . undefined ; } ) ; it ( 'should get multiple entities' , async function ( ) { const Person = setupCollection ( ) ; const persons = [ copyPerson ( ) , copyPerson ( ) , copyPerson ( ) ] ; const ids = await Person . insert ( ... persons ) ; expect ( await Promise . all ( ids . map ( ( id ) => Person . findById ( id ) ) ) ) . to . deep . equal ( persons ) ; const founds = await Promise . all ( [ 'foo' , 'bar' , 'baz' ] . map ( ( p ) => Person . findById ( p ) ) ) ; expect ( founds . every ( ( found ) => found === undefined ) ) . to . be . true ; } ) ; } ) ; describe ( 'type checking entities' , ( ) => { it ( 'should correctly handle typed entities' , async function ( ) { const Person = new Collection < Person > ( db . table ( 'Person' ) ) ; const person = copyPerson ( ) ; const typed = Person . create ( person ) ; expect ( typed . age ) . to . equal ( person . age ) ; expect ( typed . name ) . to . equal ( person . name ) ; expect ( typed . _id ) . to . not . be . undefined ; const Unknown = new Collection ( db . table ( 'Person' ) ) ; const empty = Unknown . create ( ) ; expect ( empty . exists ) . to . not . be . undefined ; expect ( empty . _id ) . to . not . be . undefined ; } ) ; } ) ; describe ( 'exporting entities' , ( ) => { it ( 'should export entity to JSON string' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; const [ id ] = await Person . insert ( person ) ; const test = await Person . has ( id ) ; expect ( test ) . to . be . true ; const personInstance = await Person . findById ( id ) ; if ( personInstance ) { const json = personInstance . toJSON ( ) ; expect ( json ) . to . deep . equal ( person ) ; } else { throw new Error ( 'should not be undefined' ) ; } } ) ; } ) ; describe ( 'deleting entities' , ( ) => { it ( 'should delete existing entity' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; let [ id ] = await Person . insert ( person ) ; expect ( await Person . has ( id ) ) . to . be . true ; await Person . delete ( id ) ; expect ( await Person . has ( id ) ) . to . be . false ; await Person . delete ( 'blah' ) ; await Person . save ( person ) ; expect ( await Person . has ( id ) ) . to . be . true ; await Person . delete ( id ) ; expect ( await Person . has ( id ) ) . to . be . false ; [ id ] = await Person . insert ( person ) ; expect ( await Person . has ( id ) ) . to . equal ( true ) ; const personInstance = await Person . findById ( id ) ; if ( personInstance ) { await personInstance . remove ( ) ; expect ( await Person . has ( id ) ) . to . equal ( false ) ; } else { throw new Error ( 'should not be undefined' ) ; } } ) ; it ( 'should delete multiple entities' , async function ( ) { const Person = setupCollection ( ) ; const persons = [ copyPerson ( ) , copyPerson ( ) , copyPerson ( ) ] ; const ids = await Person . insert ( ... persons ) ; expect ( await Promise . all ( ids . map ( ( id ) => Person . has ( id ) ) ) ) . to . deep . equal ( [ true , true , true ] ) ; await Person . delete ( ... ids ) ; expect ( await Promise . all ( ids . map ( ( p ) => Person . has ( p ) ) ) ) . to . deep . equal ( [ false , false , false ] ) ; await Person . delete ( 'foo' , 'bar' , 'baz' ) ; } ) ; it ( 'should delete all entities' , async function ( ) { const Person = setupCollection ( ) ; await Person . writeTransaction ( async function ( ) { const person = copyPerson ( ) ; await Person . insert ( person ) ; } ) ; expect ( await Person . find ( ) . count ( ) ) . to . equal ( 1 ) ; await Person . clear ( ) ; expect ( await Person . count ( ) ) . to . equal ( 0 ) ; } ) ; } ) ; describe ( 'saving entities' , ( ) => { it ( 'should save/update existing entity' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; const [ id ] = await Person . save ( person ) ; person . name = 'Mod' ; await Person . save ( person ) ; expect ( await Person . findById ( id ) ) . to . haveOwnProperty ( 'name' , 'Mod' ) ; const personInstance = await Person . findById ( id ) ; if ( personInstance ) { personInstance . age = 99 ; await personInstance . save ( ) ; expect ( await Person . findById ( id ) ) . to . haveOwnProperty ( 'age' , 99 ) ; } else { throw new Error ( 'should not be undefined' ) ; } } ) ; it ( 'should save/update multiple entities' , async function ( ) { const Person = setupCollection ( ) ; const persons = [ copyPerson ( ) , copyPerson ( ) , copyPerson ( ) ] ; await Person . insert ( ... persons ) ; persons . forEach ( ( p ) => p . age ++ ) ; await Person . save ( ... persons ) ; const array = await Person . find ( { } ) . toArray ( ( data ) => data . map ( ( { age } ) => age ) ) ; expect ( array ) . to . deep . equal ( [ 8 , 8 , 8 ] ) ; } ) ; it ( 'should also save/update a non-existent entity' , async function ( ) { const Person = setupCollection ( ) ; await Person . save ( { name : 'nothing' , age : 55 } ) ; expect ( await Person . find ( ) . count ( ) ) . to . equal ( 1 ) ; } ) ; } ) ; describe ( 'find/search' , ( ) => { it ( 'should support finding one result at a time' , async function ( ) { const Person = setupCollection ( ) ; const people = [ { name : 'Lucas' , age : 7 } , { name : 'Clyde' , age : 99 } , { name : 'Duke' , age : 2 } , ] ; await Person . insert ( ... people ) ; const query = { age : { $gt : 5 } , } ; const result = await Person . findOne ( query ) ; expect ( result ) . to . not . be . undefined ; expect ( result ) . to . have . ownProperty ( 'age' ) ; expect ( result ?. age ) . to . be . greaterThan ( 5 ) ; } ) ; it ( 'should support simple queries' , async function ( ) { const Person = setupCollection ( ) ; const people = [ { name : 'Lucas' , age : 7 } , { name : 'Clyde' , age : 99 } , { name : 'Duke' , age : 2 } , ] ; await Person . insert ( ... people ) ; const query = { age : { $gt : 5 } , } ; const results = Person . find ( query ) ; expect ( await results . count ( ) ) . to . equal ( 2 ) ; const last = await results . last ( ) ; expect ( last ) . to . have . ownProperty ( 'age' ) ; expect ( last ?. age ) . to . be . greaterThan ( 5 ) ; } ) ; it ( 'should support complex queries' , async function ( ) { const Person = setupCollection ( ) ; const people = [ { name : 'Lucas' , age : 56 } , { name : 'Clyde' , age : 55 } , { name : 'Mike' , age : 52 } , { name : 'Micheal' , age : 52 } , { name : 'Duke' , age : 2 } , { name : 'Michelle' , age : 2 } , { name : 'Michelangelo' , age : 55 } , ] ; await Person . insert ( ... people ) ; const query = { $and : [ { age : { $gt : 5 } } , { age : { $lt : 56 } } , { name : { $not : { $eq : 'Micheal' } } } , ] , } ; const results = Person . find ( query ) ; expect ( await results . count ( ) ) . to . equal ( 3 ) ; const last = await results . last ( ) ; expect ( last ) . to . have . ownProperty ( 'age' ) ; expect ( last ?. age ) . to . be . greaterThan ( 5 ) ; expect ( last ?. age ) . to . be . lessThan ( 56 ) ; expect ( await Person . find ( ) . count ( ) ) . to . equal ( 7 ) ; } ) ; } ) ; describe ( 'read transaction' , ( ) => { it ( 'should test for existing entity' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; const [ id ] = await Person . save ( person ) ; await Person . readTransaction ( async function ( ) { expect ( await Person . has ( id ) ) . to . be . true ; } ) ; } ) ; it ( 'should return existing entity' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; const [ id ] = await Person . save ( person ) ; await Person . readTransaction ( async function ( ) { const found = await Person . findById ( id ) ; expect ( found ) . to . deep . equal ( person ) ; } ) ; } ) ; it ( 'should support nested transactions, but no writes inside a read transaction' , async function ( ) { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; await Person . save ( person ) ; try { await Person . readTransaction ( async function ( ) { await Person . writeTransaction ( async function ( ) { return Person . insert ( person ) ; } ) ; throw shouldHaveThrown ; } ) ; } catch ( err ) { expect ( err ) . to . not . equal ( shouldHaveThrown ) ; } } ) ; } ) ; describe ( 'write transaction' , ( ) => { it ( 'should perform normal write operations' , async function ( ) { const Person = setupCollection ( ) ; await Person . writeTransaction ( async function ( ) { const person = copyPerson ( ) ; const [ id ] = await Person . insert ( person ) ; expect ( await Person . find ( ) . count ( ) ) . to . equal ( 1 ) ; return Person . delete ( id ) ; } ) ; expect ( await Person . find ( ) . count ( ) ) . to . equal ( 0 ) ; } ) ; it ( 'should allow read transactions inside write transactions' , ( done ) => { const Person = setupCollection ( ) ; const person = copyPerson ( ) ; Person . save ( person ) . then ( ( [ id ] ) => { Person . writeTransaction ( async function ( ) { const found = await Person . readTransaction ( async function ( ) { return Person . findById ( id ) ; } ) ; expect ( found ) . to . not . be . undefined ; } ) . then ( done ) ; } ) ; } ) . timeout ( 5000 ) ; } ) ; } ) ; } ) ; } ) ;