"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = require("chai");
const ulid_1 = require("ulid");
const mingo_1 = __importDefault(require("mingo"));
const interface_datastore_1 = require("interface-datastore");
const streaming_iterables_1 = require("streaming-iterables");
const collection_1 = require("./collection");
const personSchema = {
    $schema: 'http://json-schema.org/draft-07/schema#',
    title: 'Person',
    description: 'A simple person schema',
    type: 'object',
    properties: {
        _id: {
            description: 'The unique identifier for a person',
            type: 'string',
        },
        name: {
            description: 'Name of the person',
            type: 'string',
        },
        age: {
            type: 'number',
            minimum: 0,
            exclusiveMaximum: 100,
        },
    },
    required: ['_id', 'name', 'age'],
};
describe('Collection', () => {
    it('basic', () => __awaiter(void 0, void 0, void 0, function* () {
        const Thing = new collection_1.Collection('things', {});
        const data = { _id: '123', thing: 'one' };
        const thing1 = new Thing(data);
        chai_1.expect(thing1.thing).to.equal('one');
        thing1.other = 1;
        // thing1.more = 'something' // Won't compile unless type can have additional properties
        chai_1.expect(thing1.other).to.equal(1);
        chai_1.expect(yield streaming_iterables_1.collect(Thing.find({}))).to.have.length(0);
        yield thing1.save(); // Now saved to collection
        chai_1.expect(yield streaming_iterables_1.collect(Thing.find({}))).to.have.length(1);
        yield Thing.save(data);
        try {
            yield Thing.insert(data);
            throw new Error('should have thrown');
        }
        catch (err) {
            chai_1.expect(err).to.equal(collection_1.existingKeyError);
        }
        yield Thing.insert({ _id: '', other: -1, thing: 'five' }, { _id: '', other: 2, thing: 'two' }, { _id: '', other: 3, thing: 'three' }, { _id: '', other: 4, thing: 'four' });
        const all = yield streaming_iterables_1.collect(Thing.find({ $or: [{ other: { $gt: 1 } }, { thing: 'one' }] }, { sort: { other: -1 } }));
        const last = all[0];
        chai_1.expect(last.value).to.have.haveOwnProperty('other', 4);
    }));
    describe('complex', () => {
        const defaultPerson = Object.freeze({
            _id: '',
            name: 'Lucas',
            age: 7,
        });
        const copyPerson = (person = defaultPerson) => {
            return Object.assign({}, person);
        };
        const setupCollection = (child) => {
            return new collection_1.Collection('Person', personSchema, { child });
        };
        let store;
        beforeEach(() => {
            // Clear out the store before each run
            store = new interface_datastore_1.MemoryDatastore();
        });
        describe('top-level instance', () => {
            it('should derive a validator from an schema', () => {
                const Person = setupCollection(store);
                chai_1.expect(Person.validator(copyPerson())).to.be.true;
            });
            it('should handle multiple write operations', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                yield Person.insert(copyPerson(), copyPerson(), copyPerson());
                const person = copyPerson();
                yield new Person(person).save();
                yield Person.delete(person._id);
                // Should modify the object in-place
                chai_1.expect(person._id).to.not.equal('');
                chai_1.expect(yield streaming_iterables_1.collect(Person.find({}))).to.have.length(3);
            }));
        });
        describe('creating entities', () => {
            it('should create a single entity (w/ type checking) at a time', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person1 = copyPerson();
                yield Person.insert(person1);
                const exists = yield Person.has(person1._id);
                const obj = yield Person.findById(person1._id);
                chai_1.expect(exists).to.be.true;
                chai_1.expect(obj).to.have.ownProperty('_id', person1._id);
                const person2 = new Person(copyPerson());
                chai_1.expect(yield Person.has(person2._id)).to.be.false;
                yield person2.save();
                chai_1.expect(yield Person.has(person2._id)).to.be.true;
            }));
            it('should create multiple entities (variadic arguments w/ type checking) at once', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = copyPerson();
                yield Person.insert(person, copyPerson(), copyPerson());
                chai_1.expect(yield streaming_iterables_1.collect(Person.find({}))).to.have.length(3);
                chai_1.expect(yield Person.has(person._id)).to.be.true;
            }));
            it('should create an entity with a predefined id', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const _id = ulid_1.ulid();
                const person = new Person({ _id, name: 'Hans', age: 12 });
                chai_1.expect(person.toJSON()).to.have.ownProperty('_id', _id);
            }));
            it('should not overwrite an existing entity', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const _id = ulid_1.ulid();
                try {
                    yield Person.insert({ _id, name: 'Hans', age: 12 });
                    const person = new Person({ _id, name: 'Hans', age: 12 }); // No error yet
                    yield Person.insert(person); // This works because our encoder checks for toJSON
                    throw new Error('should not create already existing instance');
                }
                catch (err) {
                    chai_1.expect(err.toString()).to.equal('Error: Existing key');
                }
            }));
        });
        describe('creating transactions', () => {
            it('should create a readonly transaction', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = new Person(copyPerson());
                yield person.save();
                yield Person.readTransaction((r) => __awaiter(void 0, void 0, void 0, function* () {
                    chai_1.expect(yield r.has(person._id)).to.be.true;
                }));
            }));
            it('should create a write transaction', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = new Person(copyPerson());
                yield Person.writeTransaction((w) => __awaiter(void 0, void 0, void 0, function* () {
                    yield w.insert(person);
                    chai_1.expect(yield w.has(person._id)).to.be.true;
                }));
            }));
        });
        describe('checking for entities', () => {
            it('should test for existing entity', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = copyPerson();
                yield Person.insert(person);
                chai_1.expect(yield Person.has(person._id)).to.be.true;
                chai_1.expect(yield Person.has('blah')).to.be.false;
                const person2 = new Person(copyPerson());
                yield person2.save();
                chai_1.expect(yield person2.exists()).to.be.true;
                yield person2.remove();
                chai_1.expect(yield person2.exists()).to.be.false;
            }));
            it('should test for multiple entities', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const persons = [copyPerson(), copyPerson(), copyPerson()];
                yield Person.insert(...persons);
                chai_1.expect(yield Promise.all(persons.map((p) => Person.has(p._id)))).to.deep.equal([
                    true,
                    true,
                    true,
                ]);
                chai_1.expect(yield Promise.all(['foo', 'bar', 'baz'].map((p) => Person.has(p)))).to.deep.equal([
                    false,
                    false,
                    false,
                ]);
            }));
        });
        describe('returning entities', () => {
            it('should get existing entity', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = new Person(copyPerson());
                yield person.save();
                const found = yield Person.findById(person._id);
                chai_1.expect(found).to.deep.equal(person.toJSON());
                try {
                    yield Person.findById('blah');
                    throw new Error('should throw on invalid id');
                }
                catch (err) {
                    chai_1.expect(err.toString()).to.equal('Error: Not Found');
                }
            }));
            it('should get multiple entities', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const persons = [copyPerson(), copyPerson(), copyPerson()];
                yield Person.insert(...persons);
                chai_1.expect(yield Promise.all(persons.map((p) => Person.findById(p._id)))).to.deep.equal(persons);
                try {
                    yield Promise.all(['foo', 'bar', 'baz'].map((p) => Person.findById(p)));
                    throw new Error('should throw on invalid id');
                }
                catch (err) {
                    chai_1.expect(err.toString()).to.equal('Error: Not Found');
                }
            }));
        });
        describe('deleting for entities', () => {
            it('should delete existing entity', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = new Person(copyPerson());
                yield person.save();
                chai_1.expect(yield Person.has(person._id)).to.be.true;
                yield Person.delete(person._id);
                chai_1.expect(yield Person.has(person._id)).to.be.false;
                yield Person.delete('blah'); // Should not throw here, fails gracefully
                yield person.save();
                chai_1.expect(yield Person.has(person._id)).to.be.true;
                yield person.remove();
                chai_1.expect(yield Person.has(person._id)).to.be.false;
            }));
            it('should delete multiple entities', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const persons = [copyPerson(), copyPerson(), copyPerson()];
                yield Person.insert(...persons);
                const ids = persons.map((p) => p._id);
                chai_1.expect(yield Promise.all(persons.map((p) => Person.has(p._id)))).to.deep.equal([
                    true,
                    true,
                    true,
                ]);
                yield Person.delete(...ids);
                chai_1.expect(yield Promise.all(ids.map((p) => Person.has(p)))).to.deep.equal([
                    false,
                    false,
                    false,
                ]);
                yield Person.delete('foo', 'bar', 'baz'); // Should not error
            }));
        });
        describe('saving entities', () => {
            it('should save/update existing entity', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = new Person(copyPerson());
                yield person.save();
                person.name = 'Mod'; // ;)
                yield person.save();
                chai_1.expect(yield Person.findById(person._id)).to.haveOwnProperty('name', 'Mod');
            }));
            it('should save/update multiple entities', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const persons = [copyPerson(), copyPerson(), copyPerson()];
                yield Person.insert(...persons);
                persons.forEach((p) => p.age++);
                yield Person.save(...persons);
                const array = yield streaming_iterables_1.collect(Person.find({}));
                chai_1.expect(array.map((p) => p.value.age)).to.deep.equal([8, 8, 8]);
            }));
            it('should also save/update a non-existant entity', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                yield Person.save({ _id: '', name: 'nothing', age: 55 });
                chai_1.expect(yield streaming_iterables_1.collect(Person.find())).to.have.length(1);
            }));
        });
        describe('find/search', () => {
            it('should support simple queries', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const people = [
                    { _id: '', name: 'Lucas', age: 7 },
                    { _id: '', name: 'Clyde', age: 99 },
                    { _id: '', name: 'Duke', age: 2 },
                ];
                yield Person.insert(...people);
                const query = {
                    // Find everyone over the age of 5
                    age: { $gt: 5 },
                };
                const results = yield streaming_iterables_1.collect(Person.find(query));
                chai_1.expect(results).to.have.length(2);
                const last = results.pop();
                // Should we 'unravel' the key/value pairs here?
                chai_1.expect(last === null || last === void 0 ? void 0 : last.value).to.have.ownProperty('age');
                chai_1.expect(last === null || last === void 0 ? void 0 : last.value.age).to.be.greaterThan(5);
            }));
            it('should support complex queries', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const people = [
                    { _id: '', name: 'Lucas', age: 56 },
                    { _id: '', name: 'Clyde', age: 55 },
                    { _id: '', name: 'Mike', age: 52 },
                    { _id: '', name: 'Micheal', age: 52 },
                    { _id: '', name: 'Duke', age: 2 },
                    { _id: '', name: 'Michelle', age: 2 },
                    { _id: '', name: 'Michelangelo', age: 55 },
                ];
                yield Person.insert(...people);
                const query = {
                    // Find people who are older than 5, and younger than 56, ...
                    // and who's name starts with Mi or is Clyde, ...
                    // but don't include Michael, he's a jerk...
                    $and: [
                        { age: { $gt: 5 } },
                        { age: { $lt: 56 } },
                        { $or: [{ name: { $regex: '^Mi' } }, { name: { $eq: 'Clyde' } }] },
                        { name: { $not: { $eq: 'Micheal' } } },
                    ],
                };
                const results = yield streaming_iterables_1.collect(Person.find(query));
                chai_1.expect(results).to.have.length(3);
                const last = results.pop();
                chai_1.expect(last === null || last === void 0 ? void 0 : last.value).to.have.ownProperty('age');
                chai_1.expect(last === null || last === void 0 ? void 0 : last.value.age).to.be.greaterThan(5);
                chai_1.expect(last === null || last === void 0 ? void 0 : last.value.age).to.be.lessThan(56);
                chai_1.expect(yield streaming_iterables_1.collect(Person.find())).to.have.length(7);
                // Use mingo directly, should also return 3 (sanity check)
                chai_1.expect(mingo_1.default.find(people, query).count()).to.equal(3);
            }));
        });
        describe('read transaction', () => {
            it('should test for existing entity', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = new Person(copyPerson());
                yield person.save();
                yield Person.readTransaction((c) => __awaiter(void 0, void 0, void 0, function* () {
                    chai_1.expect(yield c.has(person._id)).to.be.true;
                }));
            }));
            it('should return existing entity', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = new Person(copyPerson());
                yield person.save();
                yield Person.readTransaction((c) => __awaiter(void 0, void 0, void 0, function* () {
                    const found = yield c.findById(person._id);
                    chai_1.expect(found).to.deep.equal(person.toJSON());
                    // await c.insert(person) // Compiler won't let us!
                }));
            }));
            it('should support a timeout, and preclude any write transactions until done', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = new Person(copyPerson());
                yield person.save();
                const t1 = Date.now();
                yield Person.readTransaction((c) => __awaiter(void 0, void 0, void 0, function* () {
                    try {
                        // Start a deadlock...
                        yield Person.writeTransaction((w) => __awaiter(void 0, void 0, void 0, function* () {
                            yield w.insert(person); // Won't ever run
                        }), 2000);
                        throw new Error('should not be able to aquire this lock');
                    }
                    catch (err) {
                        chai_1.expect(err.toString()).to.equal('Error: acquire lock timeout');
                    }
                }));
                const t2 = Date.now();
                chai_1.expect(t2 - t1 + 100).to.be.greaterThan(2000); // Adjust up to catch approx. timings
            })).timeout(3000);
        });
        describe('write transaction', () => {
            it('should perform normal write operations', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                let count = 0;
                Person.child.on('events', () => count++);
                yield Person.writeTransaction((w) => __awaiter(void 0, void 0, void 0, function* () {
                    const person = new Person(copyPerson());
                    yield w.insert(person);
                    yield w.delete(person._id);
                }));
                chai_1.expect(count).to.equal(2);
            }));
            it('should support a timeout, and preclude any read transactions until done', () => __awaiter(void 0, void 0, void 0, function* () {
                const Person = setupCollection(store);
                const person = new Person(copyPerson());
                yield person.save();
                const t1 = Date.now();
                yield Person.writeTransaction((w) => __awaiter(void 0, void 0, void 0, function* () {
                    try {
                        // Start a deadlock...
                        yield Person.readTransaction((r) => __awaiter(void 0, void 0, void 0, function* () {
                            yield r.findById(person._id); // Won't ever run
                        }), 2000);
                        throw new Error('should not be able to aquire this lock');
                    }
                    catch (err) {
                        chai_1.expect(err.toString()).to.equal('Error: acquire lock timeout');
                    }
                }));
                const t2 = Date.now();
                chai_1.expect(t2 - t1 + 100).to.be.greaterThan(2000); // Adjust up to catch approx. timings
            })).timeout(3000);
        });
    });
});
//# sourceMappingURL=collection.spec.js.map