import { Context } from '@textile/context' ; import { Identity , PrivateKey } from '@textile/crypto' ; import { ThreadID } from '@textile/threads-id' ; import { expect } from 'chai' ; import { Client , getFunctionBody , JSONSchema3or4 , Update } from './index' ; import { Event , ReadTransaction , Where , WriteTransaction } from './models' ; const personSchema = { $id : 'https://example.com/person.schema.json' , $schema : 'http://json-schema.org/draft-07/schema#' , title : 'Person' , type : 'object' , required : [ '_id' ] , properties : { _id : { type : 'string' , description : "The instance's id." , } , firstName : { type : 'string' , description : "The person's first name." , } , lastName : { type : 'string' , description : "The person's last name." , } , age : { description : 'Age in years which must be equal to or greater than zero.' , type : 'integer' , minimum : 0 , } , } , } ; const schema2 = { properties : { _id : { type : 'string' } , fullName : { type : 'string' } , age : { type : 'integer' , minimum : 0 } , } , } ; interface Person { _id : string ; firstName : string ; lastName : string ; age : number ; } const createPerson = ( ) : Person => { return { _id : '' , firstName : 'Adam' , lastName : 'Doe' , age : 21 , } ; } ; describe ( 'Thread Client' , function ( ) { const dbID = ThreadID . fromRandom ( ) ; let dbKey ; let identity ; const client = new Client ( new Context ( 'http://127.0.0.1:6007' ) ) ; before ( async function ( ) { identity = PrivateKey . fromRandom ( ) ; await client . getToken ( identity ) ; await client . newDB ( dbID , 'test' ) ; } ) ; describe ( 'Collections' , function ( ) { it ( 'newCollection should work and create an empty object' , async function ( ) { const register = await client . newCollection ( dbID , { name : 'Person' , schema : personSchema , } ) ; expect ( register ) . to . be . undefined ; } ) ; it ( 'newCollectionFromObject should create new collections from input objects' , async function ( ) { const register = await client . newCollectionFromObject ( dbID , { _id : '' , these : 'can' , all : 83 , values : [ 'that' , 'arent' , 'already' ] , specified : true , } , { name : 'FromObject' , } ) ; expect ( register ) . to . be . undefined ; } ) ; it ( 'updateCollection should update an existing collection' , async function ( ) { await client . updateCollection ( dbID , { name : 'FromObject' , schema : schema2 , indexes : [ { path : 'age' , unique : false , } , ] , } ) ; await client . create ( dbID , 'FromObject' , [ { _id : '' , fullName : 'Madonna' , age : 0 , } , ] ) ; } ) ; it ( 'getCollectionInfo should return information about a collection' , async function ( ) { const info = await client . getCollectionInfo ( dbID , 'FromObject' ) ; expect ( info . name ) . to . equal ( 'FromObject' ) ; expect ( info . schema ) . to . deep . equal ( { properties : { _id : { type : 'string' , } , age : { type : 'integer' , } , fullName : { type : 'string' , } , } , } ) ; expect ( info . indexes ) . to . have . lengthOf ( 1 ) ; } ) ; it ( 'getCollectionInfo should throw for a missing collection' , async function ( ) { try { await client . getCollectionInfo ( dbID , 'Fake' ) ; throw new Error ( 'should have thrown' ) ; } catch ( err ) { expect ( err . toString ( ) ) . to . include ( 'collection not found' ) ; } } ) ; it ( 'getCollectionIndexes should list valid collection indexes' , async function ( ) { const list = await client . getCollectionIndexes ( dbID , 'FromObject' ) ; expect ( list ) . to . have . length ( 1 ) ; } ) ; it ( 'deleteCollection should delete an existing collection' , async function ( ) { await client . deleteCollection ( dbID , 'FromObject' ) ; try { await client . create ( dbID , 'FromObject' , [ { _id : 'blah' , nothing : 'weve' , seen : 84 , } , ] ) ; throw new Error ( 'should have thrown' ) ; } catch ( err ) { expect ( err . toString ( ) ) . to . include ( 'collection not found' ) ; } } ) ; it ( 'ListCollections should return a list of collections' , async function ( ) { const list = await client . listCollections ( dbID ) ; expect ( list ) . to . have . length ( 1 ) ; } ) ; } ) ; describe ( '.listDBs' , function ( ) { it ( 'should list the correct number of dbs with the correct name' , async function ( ) { const id2 = ThreadID . fromRandom ( ) ; const random = Math . floor ( Math . random ( ) * 10 ) ; const name2 = `db2-${ random }` ; await client . newDB ( id2 , name2 ) ; const list = await client . listDBs ( ) ; expect ( list . length ) . to . be . greaterThan ( 1 ) ; expect ( list . map ( ( db ) => db . name ) ) . to . include ( name2 ) ; } ) ; } ) ; describe ( '.getDBInfo' , function ( ) { it ( 'should return a valid db info object' , async function ( ) { const invites = await client . getDBInfo ( dbID ) ; expect ( invites ) . to . not . be . undefined ; expect ( invites . addrs [ 0 ] ) . to . not . be . undefined ; expect ( invites . key ) . to . not . be . undefined ; dbKey = invites . key ; expect ( invites ) . to . not . be . empty ; } ) ; } ) ; describe ( '.deleteDB' , function ( ) { it ( 'should cleanly delete a database' , async function ( ) { const id = ThreadID . fromRandom ( ) ; await client . newDB ( id ) ; const before = ( await client . listDBs ( ) ) . length ; await client . deleteDB ( id ) ; const after = ( await client . listDBs ( ) ) . length ; expect ( before ) . to . equal ( after + 1 ) ; try { await client . getDBInfo ( id ) ; throw new Error ( 'should have thrown' ) ; } catch ( err ) { expect ( err . toString ( ) ) . to . include ( 'thread not found' ) ; } } ) . timeout ( 5000 ) ; } ) ; describe ( '.newDBFromAddr' , function ( ) { const client2 = new Client ( new Context ( 'http://127.0.0.1:6207' ) ) ; before ( async function ( ) { identity = PrivateKey . fromRandom ( ) ; await client2 . getToken ( identity ) ; } ) ; it ( 'response should contain a valid list of thread protocol addrs' , async function ( ) { const info = await client . getDBInfo ( dbID ) ; info . addrs . forEach ( ( addr ) => { addr . replace ( '/ip4/127.0.0.1' , '/dns4/threads1/' ) ; } ) ; const id1 = await client2 . joinFromInfo ( info , false , [ { name : 'Person' , schema : personSchema , } , { name : 'FromObject' , schema : schema2 , } , ] ) ; expect ( id1 . equals ( dbID ) ) . to . equal ( true ) ; const info2 = await client2 . getDBInfo ( dbID ) ; expect ( info2 . addrs . length ) . to . be . greaterThan ( 1 ) ; expect ( info2 . key ) . to . equal ( info . key ) ; try { const id2 = await client2 . newDBFromAddr ( info . addrs [ 0 ] , dbKey , [ ] ) ; expect ( id2 . equals ( id1 ) ) . to . equal ( true ) ; } catch ( err ) { expect ( err . toString ( ) ) . to . include ( 'already exists' ) ; } } ) ; } ) ; describe ( '.create' , function ( ) { it ( 'response should contain a JSON parsable instancesList' , async function ( ) { const instances = await client . create ( dbID , 'Person' , [ createPerson ( ) ] ) ; expect ( instances . length ) . to . equal ( 1 ) ; } ) ; } ) ; describe ( '.readFilter' , function ( ) { interface Dog { _id : string ; name : string ; comments : any [ ] ; } const readFilter = ( _reader , instance ) => { instance . name = 'Clyde' ; return instance ; } ; it ( 'should modify returned instances before they are received by the client' , async function ( ) { await client . newCollection ( dbID , { name : 'Dog' , readFilter , } ) ; const dog = { _id : '' , name : 'Fido' , comments : [ ] } ; const [ id ] = await client . create ( dbID , 'Dog' , [ dog ] ) ; const res = await client . findByID < Dog > ( dbID , 'Dog' , id ) ; expect ( res . name ) . to . equal ( 'Clyde' ) ; } ) ; } ) ; describe ( '.writeValidator' , function ( ) { interface Dog { _id : string ; name : string ; comments : any [ ] ; } const writeValidator = ( _writer , event , _instance ) => { var type = event . patch . type ; var patch = event . patch . json_patch ; switch ( type ) { case 'delete' : return false ; default : if ( patch . name !== 'Fido' && patch . name != 'Clyde' ) { return false ; } return true ; } } ; it ( 'should handle string-based writeValidators' , async function ( ) { const replaceThisValidator = ( writer ) => { const ownerPub = 'replaceThis' ; if ( writer === ownerPub ) { return true ; } return false ; } ; const writeValidatorString = getFunctionBody ( replaceThisValidator ) . replace ( 'replaceThis' , 'publicKeyString' ) ; await client . newCollection ( dbID , { name : 'DontUseThisOne' , writeValidator : writeValidatorString , } ) ; } ) ; it ( 'should catch invalid write operations before they are added to the db' , async function ( ) { await client . updateCollection ( dbID , { name : 'Dog' , writeValidator , } ) ; const dog = { _id : '' , name : 'Fido' , comments : [ ] } ; const [ id ] = await client . create ( dbID , 'Dog' , [ dog ] ) ; dog . _id = id ; dog . name = 'Bob' ; try { await client . save ( dbID , 'Dog' , [ dog ] ) ; throw new Error ( 'should have failed' ) ; } catch ( err ) { expect ( err . message ) . to . include ( 'app denied net record body' ) ; } dog . name = 'Clyde' ; await client . save ( dbID , 'Dog' , [ dog ] ) ; try { await client . delete ( dbID , 'Dog' , [ id ] ) ; throw new Error ( 'should have failed' ) ; } catch ( err ) { expect ( err . message ) . to . include ( 'app denied net record body' ) ; } } ) ; } ) ; describe ( '.verify' , function ( ) { const writeValidator = ( _writer , event , _instance ) => { var type = event . patch . type ; var patch = event . patch . json_patch ; switch ( type ) { case 'delete' : return false ; default : if ( patch . age > 50 ) { return false ; } return true ; } } ; it ( 'should be able to verfiy incoming instances before writing/saving' , async function ( ) { await client . newCollection ( dbID , { name : 'Verified' , schema : personSchema , writeValidator , } ) ; const person = createPerson ( ) ; const [ id ] = await client . create ( dbID , 'Verified' , [ person ] ) ; expect ( id ) . to . not . be . undefined ; person . _id = id ; person . age = 51 ; try { await client . verify ( dbID , 'Verified' , [ person ] ) ; throw new Error ( 'wrong error' ) ; } catch ( err ) { expect ( err . message ) . to . include ( 'app denied net record body' ) ; } person . age = 50 ; const err = await client . verify ( dbID , 'Verified' , [ person ] ) ; expect ( err ) . to . be . undefined ; } ) ; } ) ; describe ( '.save' , function ( ) { it ( 'response should be defined and be an empty object' , async function ( ) { const person = createPerson ( ) ; const instances = await client . create ( dbID , 'Person' , [ person ] ) ; expect ( instances . length ) . to . equal ( 1 ) ; person . _id = instances [ 0 ] ; person ! . age = 30 ; const save = await client . save ( dbID , 'Person' , [ person ] ) ; expect ( save ) . to . be . undefined ; } ) ; } ) ; describe ( '.delete' , function ( ) { it ( 'response should be defined and be an empty object' , async function ( ) { const instances = await client . create ( dbID , 'Person' , [ createPerson ( ) ] ) ; expect ( instances . length ) . to . equal ( 1 ) ; const personID = instances [ 0 ] ; const deleted = await client . delete ( dbID , 'Person' , [ personID ] ) ; expect ( deleted ) . to . be . undefined ; } ) ; } ) ; describe ( 'create and delete many instances' , function ( ) { it ( 'should not timeout or lead to hanging transaction' , async function ( ) { const many = Array . from ( { length : 100 } , ( ) => createPerson ( ) ) ; const ids = await client . create ( dbID , 'Person' , many ) ; expect ( ids . length ) . to . equal ( many . length ) ; const deleted = await client . delete ( dbID , 'Person' , ids ) ; expect ( deleted ) . to . be . undefined ; } ) . timeout ( 10000 ) ; } ) ; describe ( '.has' , function ( ) { it ( 'the created object should also return true for has' , async function ( ) { const instances = await client . create ( dbID , 'Person' , [ createPerson ( ) ] ) ; expect ( instances . length ) . to . equal ( 1 ) ; const has = await client . has ( dbID , 'Person' , instances ) ; expect ( has ) . to . be . true ; } ) . timeout ( 5000 ) ; } ) ; describe ( '.find' , function ( ) { it ( 'response should contain the same instance based on query' , async function ( ) { const frank = createPerson ( ) ; frank . firstName = 'Frank' ; const instances = await client . create ( dbID , 'Person' , [ frank ] ) ; expect ( instances . length ) . to . equal ( 1 ) ; const personID = instances [ 0 ] ; const q = new Where ( 'firstName' ) . eq ( frank . firstName ) ; const found = await client . find < Person > ( dbID , 'Person' , q ) ; expect ( found ) . to . have . length ( 1 ) ; const foundPerson = found . pop ( ) ! ; expect ( foundPerson ) . to . not . be . undefined ; expect ( foundPerson ) . to . have . property ( 'firstName' , 'Frank' ) ; expect ( foundPerson ) . to . have . property ( 'lastName' , 'Doe' ) ; expect ( foundPerson ) . to . have . property ( 'age' , 21 ) ; expect ( foundPerson ) . to . have . property ( '_id' ) ; expect ( foundPerson [ '_id' ] ) . to . equal ( personID ) ; } ) ; } ) ; describe ( '.findById' , function ( ) { it ( 'response should contain an instance' , async function ( ) { try { await client . findByID < Person > ( dbID , 'Person' , 'blah' ) ; throw new Error ( 'wrong error' ) ; } catch ( err ) { expect ( err . toString ( ) ) . to . not . include ( 'wrong error' ) ; } const [ id ] = await client . create ( dbID , 'Person' , [ createPerson ( ) ] ) ; const instance = await client . findByID < Person > ( dbID , 'Person' , id ) ; expect ( instance ) . to . not . be . undefined ; expect ( instance ) . to . have . property ( 'firstName' , 'Adam' ) ; expect ( instance ) . to . have . property ( 'lastName' , 'Doe' ) ; expect ( instance ) . to . have . property ( 'age' , 21 ) ; expect ( instance ) . to . have . property ( '_id' ) ; } ) ; } ) ; describe ( 'cross-collection ids' , function ( ) { it ( 'should not require instance ids to be unique across collections' , async function ( ) { const person = createPerson ( ) ; person . _id = 'something-unique' ; const [ first ] = await client . create ( dbID , 'Person' , [ person ] ) ; const [ second ] = await client . create ( dbID , 'Verified' , [ person ] ) ; expect ( first ) . to . equal ( second ) ; } ) ; } ) ; describe ( '.readTransaction' , function ( ) { let existingPersonID ; let transaction ; before ( async function ( ) { const instances = await client . create ( dbID , 'Person' , [ createPerson ( ) ] ) ; existingPersonID = instances . pop ( ) ! ; transaction = client . readTransaction ( dbID , 'Person' ) ; } ) ; it ( 'should start a transaction' , async function ( ) { expect ( transaction ) . to . not . be . undefined ; await transaction ! . start ( ) ; } ) ; it ( 'should able to check for an existing instance' , async function ( ) { const has = await transaction ! . has ( [ existingPersonID ] ) ; expect ( has ) . to . be . true ; } ) ; it ( 'should be able to find an existing instance' , async function ( ) { const instance = await transaction ! . findByID < Person > ( existingPersonID ) ; expect ( instance ) . to . not . be . undefined ; expect ( instance ) . to . have . property ( 'firstName' , 'Adam' ) ; expect ( instance ) . to . have . property ( 'lastName' , 'Doe' ) ; expect ( instance ) . to . have . property ( 'age' , 21 ) ; expect ( instance ) . to . have . property ( '_id' ) ; expect ( instance ?. _id ) . to . deep . equal ( existingPersonID ) ; } ) ; it ( 'should be able to close/end an transaction' , async function ( ) { await transaction ! . end ( ) ; } ) ; it ( 'should throw on invalid transaction information' , async function ( ) { const t = client . readTransaction ( dbID , 'fake' ) ; await t . start ( ) ; try { await t . has ( [ 'anything' ] ) ; throw new Error ( 'should have thrown' ) ; } catch ( err ) { expect ( err . toString ( ) ) . to . include ( 'collection not found' ) ; } } ) ; } ) ; describe ( '.writeTransaction' , function ( ) { let existingPersonID ; let transaction ; context ( 'complete transaction' , function ( ) { const person = createPerson ( ) ; before ( async function ( ) { const instances = await client . create ( dbID , 'Person' , [ person ] ) ; existingPersonID = instances . length ? instances [ 0 ] : '' ; person [ '_id' ] = existingPersonID ; transaction = client . writeTransaction ( dbID , 'Person' ) ; } ) ; it ( 'should start a transaction' , async function ( ) { expect ( transaction ) . to . not . be . undefined ; await transaction ! . start ( ) ; } ) ; it ( 'should be able to create an instance' , async function ( ) { const newPerson = createPerson ( ) ; const entities = await transaction ! . create < Person > ( [ newPerson ] ) ; expect ( entities ) . to . not . be . undefined ; expect ( entities ! . length ) . to . equal ( 1 ) ; } ) ; it ( 'should able to check for an existing instance' , async function ( ) { const has = await transaction ! . has ( [ existingPersonID ] ) ; expect ( has ) . to . be . true ; } ) ; it ( 'should be able to find an existing instance' , async function ( ) { const instance = await transaction ! . findByID < Person > ( existingPersonID ) ; expect ( instance ) . to . not . be . undefined ; expect ( instance ) . to . have . property ( 'firstName' , 'Adam' ) ; expect ( instance ) . to . have . property ( 'lastName' , 'Doe' ) ; expect ( instance ) . to . have . property ( 'age' , 21 ) ; expect ( instance ) . to . have . property ( '_id' ) ; expect ( instance ?. _id ) . to . deep . equal ( existingPersonID ) ; } ) ; it ( 'should be able to validate an instance that is invalid' , async function ( ) { try { await transaction ?. verify ( [ createPerson ( ) ] ) ; } catch ( err ) { expect ( err . message ) . to . not . include ( 'unkown instance' ) ; } } ) ; it ( 'should be able to save an existing instance' , async function ( ) { person . age = 99 ; const saved = await transaction ! . save ( [ person ] ) ; expect ( saved ) . to . be . undefined ; const deleted = await transaction ! . delete ( [ person . _id ] ) ; expect ( deleted ) . to . be . undefined ; } ) ; it ( 'should be able to close/end an transaction' , async function ( ) { await transaction ! . end ( ) ; } ) ; } ) ; context ( 'rejected transaction' , function ( ) { it ( 'should not commit a discarded write transaction' , async function ( ) { const newPerson = createPerson ( ) ; const [ id ] = await client . create ( dbID , 'Person' , [ newPerson ] ) ; newPerson . _id = id ; const transaction = client . writeTransaction ( dbID , 'Person' ) ; await transaction . start ( ) ; newPerson . age = 38 ; const rejected = await transaction . discard ( ) ; expect ( rejected ) . to . be . undefined ; await transaction . save ( [ newPerson ] ) ; try { await transaction . end ( ) ; throw new Error ( 'wrong error' ) ; } catch ( err ) { expect ( err . message ) . to . include ( 'discarded/committed txn' ) ; } const instance = await client . findByID ( dbID , 'Person' , id ) ; expect ( instance . age ) . to . not . equal ( 38 ) ; } ) ; } ) ; } ) ; describe ( '.listen' , function ( ) { const listener = { events : 0 } ; const person = createPerson ( ) ; before ( async function ( ) { const entities = await client . create ( dbID , 'Person' , [ person ] ) ; person . _id = entities [ 0 ] ; } ) ; it ( 'should stream responses.' , ( done ) => { const callback = ( reply ? , err ? ) => { if ( err ) { throw err ; } const instance = reply ?. instance ; expect ( instance ) . to . not . be . undefined ; expect ( instance ) . to . have . property ( 'age' ) ; expect ( instance ?. age ) . to . be . greaterThan ( 21 ) ; listener . events += 1 ; if ( listener . events > 1 && listener . close ) { listener . close ( ) ; } if ( listener . events == 2 ) { done ( ) ; } } ; const closer = client . listen < Person > ( dbID , [ { collectionName : 'Person' , actionTypes : [ 'ALL' ] , } , ] , callback ) ; setTimeout ( ( ) => { const person = createPerson ( ) ; person . age = 40 ; client . create ( dbID , 'Person' , [ person ] ) ; client . create ( dbID , 'Person' , [ person ] ) ; } , 500 ) ; listener . close = ( ) => closer . close ( ) ; } ) . timeout ( 5000 ) ; it ( 'should handle deletes.' , ( done ) => { const callback = ( reply ? , err ? ) => { if ( err ) { throw err ; } expect ( reply ?. instance ) . to . be . undefined ; if ( listener . close ) { listener . close ( ) ; } done ( ) ; } ; const closer = client . listen < Person > ( dbID , [ { collectionName : 'Person' , actionTypes : [ 'DELETE' ] , } , ] , callback ) ; setTimeout ( ( ) => { const person = createPerson ( ) ; client . create ( dbID , 'Person' , [ person ] ) . then ( ( ids ) => { setTimeout ( ( ) => { client . delete ( dbID , 'Person' , ids ) ; } , 500 ) ; } ) ; } , 500 ) ; listener . close = ( ) => closer . close ( ) ; } ) . timeout ( 5000 ) ; } ) ; describe ( 'Query' , function ( ) { before ( async function ( ) { const people = [ ... Array ( 8 ) ] . map ( ( _ , i ) => { const person = createPerson ( ) ; person . age = 60 + i ; return person ; } ) ; await client . create ( dbID , 'Person' , people ) ; } ) ; it ( 'Should return a full list of entities matching the given query' , async function ( ) { const q = new Where ( 'age' ) . ge ( 60 ) . and ( 'age' ) . lt ( 66 ) . or ( new Where ( 'age' ) . eq ( 67 ) ) . skipNum ( 1 ) . limitTo ( 5 ) . orderByID ( ) ; const instances = await client . find < Person > ( dbID , 'Person' , q ) ; expect ( instances ) . to . have . length ( 5 ) ; } ) ; } ) ; describe ( 'Invalid states' , function ( ) { it ( 'should not compromise the thread to try to create the same object twice' , async function ( ) { const newPerson = createPerson ( ) ; newPerson . _id = 'new-person-id_one' ; const [ id ] = await client . create ( dbID , 'Person' , [ newPerson ] ) ; expect ( id ) . to . not . be . undefined ; try { await client . create ( dbID , 'Person' , [ newPerson ] ) ; throw new Error ( 'wrong error' ) ; } catch ( err ) { expect ( err . message ) . to . include ( 'existing instance' ) ; } const list = await client . find ( dbID , 'Person' , { } ) ; expect ( list . length ) . to . be . greaterThan ( 0 ) ; } ) ; it ( 'should also be safe inside a write transaction' , async function ( ) { const newPerson = createPerson ( ) ; newPerson . _id = 'new-person-id-two' ; const transaction = client . writeTransaction ( dbID , 'Person' ) ; await transaction . start ( ) ; const result = await transaction . create ( [ newPerson ] ) ; expect ( result ) . to . not . be . undefined ; await transaction . create ( [ newPerson ] ) ; try { await transaction . end ( ) ; throw new Error ( 'wrong error' ) ; } catch ( err ) { expect ( err . message ) . to . include ( 'already existent instance' ) ; } const list = await client . find ( dbID , 'Person' , { } ) ; expect ( list . length ) . to . be . greaterThan ( 0 ) ; const collections = await client . listCollections ( dbID ) ; expect ( collections ) . to . have . lengthOf ( 4 ) ; } ) ; } ) ; describe ( 'Restart' , function ( ) { it ( 'Should handle a whole new "restart" of the client' , async function ( ) { const newClient = new Client ( new Context ( 'http://127.0.0.1:6007' ) ) ; const person = createPerson ( ) ; await newClient . getToken ( identity ) ; const created = await newClient . create ( dbID , 'Person' , [ person ] ) ; const got = await newClient . findByID ( dbID , 'Person' , created [ 0 ] ) ; expect ( got ) . to . haveOwnProperty ( '_id' , created [ 0 ] ) ; expect ( got ) . to . haveOwnProperty ( 'firstName' , 'Adam' ) ; } ) . timeout ( 5000 ) ; } ) ; } ) ;