"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
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());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Collection = exports.ReadonlyCollection = exports.Document = exports.existingKeyError = exports.resolve = void 0;
const interface_datastore_1 = require("interface-datastore");
const ajv_1 = __importStar(require("ajv"));
const ulid_1 = require("ulid");
const streaming_iterables_1 = require("streaming-iterables");
const mingo_1 = require("mingo");
const threads_store_1 = require("@textile/threads-store");
exports.resolve = (object, path, defaultValue) => path
    .split(/[\.\[\]\'\"]/)
    .filter((p) => p)
    .reduce((o, p) => (o ? o[p] : defaultValue), object);
exports.existingKeyError = new Error('Existing key');
const defaultOptions = {
    child: new interface_datastore_1.MemoryDatastore(),
    dispatcher: new threads_store_1.Dispatcher(),
};
const cmp = (a, b, asc = 1) => {
    if (a < b)
        return -1 * asc;
    if (a > b)
        return 1 * asc;
    return 0;
};
const handler = (obj) => {
    return {
        get: (target, property, _receiver) => {
            if (Reflect.has(obj, property)) {
                return Reflect.get(obj, property);
            }
            else if (!Reflect.has(target, property)) {
                return undefined;
            }
            return Reflect.get(target, property);
        },
        set: (_target, property, value, _receiver) => {
            return Reflect.set(obj, property, value);
        },
    };
};
/**
 * Document is a wrapper around a Collection and a (proxy to) an Instance object.
 */
class Document {
    constructor(_collection, _data) {
        this._collection = _collection;
        this._data = _data;
        return new Proxy(this, handler(this._data));
    }
    /**
     * Save this Instance to its parent collection.
     */
    save() {
        return this._collection.save(this._data);
    }
    /**
     * Remove this Instance (by id) from its parent collection.
     */
    remove() {
        return this._collection.delete(this._data._id);
    }
    /**
     * Check if this Instance (by id) exists in its parent collection.
     */
    exists() {
        return this._collection.has(this._data._id);
    }
    /**
     * Get a JSON representation of this Instance.
     */
    toJSON() {
        return this._data;
    }
    toCBOR() {
        return this.toJSON();
    }
}
exports.Document = Document;
class ReadonlyCollection {
    /**
     * Collection creates a new collection.
     * @param name A name for the collection.
     * @param schema A valid JSON schema object.
     * @param options The underlying collection options.
     */
    constructor(name, schema, options = defaultOptions) {
        this.name = name;
        this.validator = new ajv_1.default({ useDefaults: true }).compile(schema);
        this.child = new threads_store_1.JsonPatchStore(options.child, new interface_datastore_1.Key(name), options.dispatcher);
    }
    static fromCollection(other) {
        const readOnly = new ReadonlyCollection(other.name, {});
        readOnly.validator = other.validator;
        readOnly.child = other.child;
        return readOnly;
    }
    /**
     * Find an Instance by _id
     * @param id The Instance id.
     */
    findById(id) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.child.get(new interface_datastore_1.Key(id));
        });
    }
    /**
     * Check that a given Instance exists.
     * @param id The Instance id.
     */
    has(id) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.child.has(new interface_datastore_1.Key(id));
        });
    }
    /**
     * Find all entities matching the query
     * @param query Mongodb-style filter query.
     * @param options Additional options to control query operation.
     */
    find(query, options = {}) {
        // @fixme(github.com/kofrasa/mingo/issues/141) Hack around some strange es issues in some JS envs
        const qry = query || {};
        Object.keys(qry).forEach((key) => {
            if (typeof qry[key] === 'object' && !Array.isArray(qry[key])) {
                qry[key] = Object.assign(new Object(), qry[key]);
            }
        });
        const m = new mingo_1.Query(qry, { idKey: '_id' });
        const filters = [({ value }) => m.test(value)];
        const orders = [];
        if (options.sort) {
            for (const [key, value] of Object.entries(options.sort)) {
                orders.push((items) => items.sort((a, b) => {
                    return cmp(exports.resolve(a.value, key), exports.resolve(b.value, key), value || 1);
                }));
            }
        }
        const q = Object.assign(Object.assign({}, options), { filters,
            orders });
        return this.child.query(q);
    }
    /**
     * Find the first Instance matching the query
     * @param query Mongodb-style filter query.
     * @param options Additional search options.
     */
    findOne(query, options = {}) {
        const it = this.find(query, options);
        return it[Symbol.asyncIterator]().next();
    }
    /**
     * Count all entities matching the query
     * @param query Mongodb-style filter query.
     * @param options Additional search options.
     */
    count(query, options = {}) {
        return streaming_iterables_1.reduce((acc, _value) => acc + 1, 0, this.find(query, options));
    }
}
exports.ReadonlyCollection = ReadonlyCollection;
/**
 * Collection is a store of entities defined by a single schema.
 */
class Collection extends ReadonlyCollection {
    /**
     * Collection creates a new collection.
     * @param name A name for the collection.
     * @param schema A valid JSON schema object.
     * @param options The underlying collection options.
     */
    constructor(name, schema, options = defaultOptions) {
        super(name, schema, options);
        this.name = name;
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const c = this;
        // Hacky function that gives us a nice ux for creating entities.
        const self = function Doc(instance) {
            if (!instance._id)
                instance._id = ulid_1.ulid();
            if (!c.validator(instance) && c.validator.errors) {
                throw new ajv_1.ValidationError(c.validator.errors);
            }
            return new Document(c, instance);
        };
        Object.setPrototypeOf(self, this.constructor.prototype);
        Object.getOwnPropertyNames(this).forEach((p) => {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            Object.defineProperty(self, p, Object.getOwnPropertyDescriptor(this, p));
        });
        return self;
    }
    /**
     * Save (multiple) entities.
     * @note Save is similar to insert, except it allows saving/overwriting existing entities.
     * @param entities A variadic array of entities.
     */
    save(...entities) {
        const batch = this.child.batch();
        for (const instance of entities) {
            if (!instance._id)
                instance._id = ulid_1.ulid();
            if (!this.validator(instance) && this.validator.errors) {
                throw new ajv_1.ValidationError(this.validator.errors);
            }
            batch.put(new interface_datastore_1.Key(instance._id), instance);
        }
        return batch.commit();
    }
    /**
     * Remove entities by id.
     * @param ids
     */
    delete(...ids) {
        const batch = this.child.batch();
        for (const id of ids) {
            batch.delete(new interface_datastore_1.Key(id));
        }
        return batch.commit();
    }
    /**
     * Insert (multiple) new instances.
     * @note Insert is similar to save, except it will not allow saving/overwriting existing entities.
     * @param instances
     */
    insert(...instances) {
        return __awaiter(this, void 0, void 0, function* () {
            // By convention we'll use insert here, but could use a specific key instead
            const lockKey = new interface_datastore_1.Key('insert');
            yield this.child.readLock(lockKey);
            try {
                const batch = this.child.batch();
                for (const instance of instances) {
                    if (!instance._id)
                        instance._id = ulid_1.ulid();
                    const key = new interface_datastore_1.Key(instance._id);
                    if (yield this.child.has(key)) {
                        throw exports.existingKeyError;
                    }
                    if (!this.validator(instance) && this.validator.errors) {
                        throw new ajv_1.ValidationError(this.validator.errors);
                    }
                    batch.put(key, instance);
                }
                return yield batch.commit();
            }
            finally {
                this.child.unlock(lockKey);
            }
        });
    }
    /**
     * Lock the collection for readonly operations.
     * @param cb A callback that takes a readonly collection.
     * @param timeout How long to wait to obtain the read lock. If after timeout seconds the lock
     * is not obtained, the method will error.
     * @note Currenrly, transactions are not atomic, or even really transactions in the normal sense.
     * They only provide locking for multi-read/single-write access to the given collection.
     */
    readTransaction(cb, timeout) {
        return __awaiter(this, void 0, void 0, function* () {
            const lockKey = new interface_datastore_1.Key('transaction');
            yield this.child.readLock(lockKey, timeout);
            try {
                return yield cb(ReadonlyCollection.fromCollection(this));
            }
            finally {
                this.child.unlock(lockKey);
            }
        });
    }
    /**
     * Lock the collection for exclusive write operations.
     * @param cb A callback that takes a collection.
     * @param timeout How long to wait to obtain the write lock. If after timeout seconds the lock
     * is not obtained, the method will error.
     * @note Currenrly, transactions are not atomic, or even really transactions in the normal sense.
     * They only provide locking for multi-read/single-write access to the given collection.
     */
    writeTransaction(cb, timeout) {
        return __awaiter(this, void 0, void 0, function* () {
            const lockKey = new interface_datastore_1.Key('transaction');
            yield this.child.writeLock(lockKey, timeout);
            try {
                return yield cb(this);
            }
            finally {
                this.child.unlock(lockKey);
            }
        });
    }
}
exports.Collection = Collection;
//# sourceMappingURL=collection.js.map