"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 __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * @packageDocumentation
 * @module @textile/threads-client
 */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const uuid = __importStar(require("uuid"));
const grpc_web_1 = require("@improbable-eng/grpc-web");
const api_pb_service_1 = require("@textile/threads-client-grpc/api_pb_service");
const api_pb_1 = require("@textile/threads-client-grpc/api_pb");
const threads_core_1 = require("@textile/threads-core");
exports.ThreadID = threads_core_1.ThreadID;
const bs58_1 = require("bs58");
const pack = __importStar(require("../package.json"));
const models_1 = require("./models");
exports.Config = models_1.Config;
exports.Query = models_1.Query;
exports.Where = models_1.Where;
/**
 * Client is a web-gRPC wrapper client for communicating with a webgRPC-enabled Textile server.
 * This client library can be used to interact with a local or remote Textile gRPC-service
 *  It is a wrapper around Textile's 'DB' API, which is defined here: https://github.com/textileio/go-threads/blob/master/api/pb/api.proto.
 */
class Client {
    /**
     * Client creates a new gRPC client instance.
     * @param host The local/remote host url. Defaults to 'localhost:6007'.
     * @param defaultTransport The default transport to use when making webgRPC calls. Defaults to WebSockets.
     */
    constructor(config = {}) {
        if (config instanceof models_1.Config) {
            this.config = config;
        }
        else {
            this.config = new models_1.Config(config.host, config.transport);
        }
        grpc_web_1.grpc.setDefaultTransport(this.config.transport);
    }
    /**
     * version is the release version.
     */
    static version() {
        return pack.version;
    }
    /**
     * newDB creates a new store on the remote node.
     * @param dbID the ID of the database
     */
    newDB(dbID) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.NewDBRequest();
            req.setDbid(dbID);
            yield this.unary(api_pb_service_1.API.NewDB, req);
            return;
        });
    }
    /**
     * newCollection registers a new model schema under the given name on the remote node.
     * The schema must be a valid json-schema.org schema, and can be a JSON string or Javascript object.
     * @param dbID the ID of the database
     * @param name The human-readable name for the model.
     * @param schema The actual json-schema.org compatible schema object.
     */
    newCollection(dbID, name, schema) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.NewCollectionRequest();
            const config = new api_pb_1.CollectionConfig();
            config.setName(name);
            config.setSchema(Buffer.from(JSON.stringify(schema)));
            req.setDbid(dbID);
            req.setConfig(config);
            yield this.unary(api_pb_service_1.API.NewCollection, req);
            return;
        });
    }
    /**
     * newDBFromAddr initializes the client with the given store, connecting to the given
     * thread address (database). It should be called before any operation on the store, and is an
     * alternative to start, which creates a local store. newDBFromAddr should also include the
     * read and follow keys, which should be Buffer, Uint8Array or base58-encoded strings.
     * See `getDBInfo` for a possible source of the address and keys.
     * @param dbID the ID of the database
     * @param address The address for the thread with which to connect.
     * Should be of the form /ip4/<url/ip-address>/tcp/<port>/p2p/<peer-id>/thread/<thread-id>
     * @param DBKey The DBKey provided through an invite or from getDBInfo.
     * @param collections An array of Name and JSON Schemas for collections in the DB.
     */
    newDBFromAddr(address, key, collections) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.NewDBFromAddrRequest();
            req.setAddr(address);
            req.setKey(typeof key === 'string' ? bs58_1.decode(key) : key);
            req.setCollectionsList(collections.map((c) => {
                const config = new api_pb_1.CollectionConfig();
                config.setName(c.name);
                config.setSchema(JSON.stringify(c.schema));
                return config;
            }));
            return yield this.unary(api_pb_service_1.API.NewDBFromAddr, req);
        });
    }
    /**
     * getDBInfo returns invite 'links' unseful for inviting other peers to join a given store/thread.
     * @param dbID the ID of the database
     */
    getDBInfo(dbID) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.GetDBInfoRequest();
            req.setDbid(dbID);
            const res = (yield this.unary(api_pb_service_1.API.GetDBInfo, req));
            const invites = [];
            for (const addr of res.addrsList) {
                const dk = Buffer.from(res.key, 'base64');
                const address = typeof addr === 'string' ? addr : Buffer.from(addr).toString();
                invites.push({
                    address,
                    key: bs58_1.encode(dk),
                });
            }
            return invites;
        });
    }
    /**
     * Creates a new model instance in the given store.
     * @param dbID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     * @param values An array of model instances as JSON/JS objects.
     */
    create(dbID, collectionName, values) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.CreateRequest();
            req.setDbid(dbID);
            req.setCollectionname(collectionName);
            const list = [];
            values.forEach((v) => {
                v['ID'] = uuid.v4();
                list.push(Buffer.from(JSON.stringify(v)));
            });
            req.setInstancesList(list);
            const res = (yield this.unary(api_pb_service_1.API.Create, req));
            return res.instanceidsList;
        });
    }
    /**
     * Saves changes to an existing model instance in the given store.
     * @param dbID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     * @param values An array of model instances as JSON/JS objects. Each model instance must have a valid existing `ID` property.
     */
    save(dbID, collectionName, values) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.SaveRequest();
            req.setDbid(dbID);
            req.setCollectionname(collectionName);
            const list = [];
            values.forEach((v) => {
                if (!v.hasOwnProperty('ID')) {
                    v['ID'] = ''; // The server will add an ID if empty.
                }
                list.push(Buffer.from(JSON.stringify(v)));
            });
            req.setInstancesList(list);
            yield this.unary(api_pb_service_1.API.Save, req);
            return;
        });
    }
    /**
     * Deletes an existing model instance from the given store.
     * @param dbID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     * @param IDs An array of instance ids to delete.
     */
    delete(dbID, collectionName, IDs) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.DeleteRequest();
            req.setDbid(dbID);
            req.setCollectionname(collectionName);
            req.setInstanceidsList(IDs);
            yield this.unary(api_pb_service_1.API.Delete, req);
            return;
        });
    }
    /**
     * has checks whether a given instance exists in the given store.
     * @param dbID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     * @param IDs An array of instance ids to check for.
     */
    has(dbID, collectionName, IDs) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.HasRequest();
            req.setDbid(dbID);
            req.setCollectionname(collectionName);
            req.setInstanceidsList(IDs);
            const res = (yield this.unary(api_pb_service_1.API.Has, req));
            return res.exists;
        });
    }
    /**
     * find queries the store for entities matching the given query parameters.
     * @param dbID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     * @param query The object that describes the query. User Query class or primitive QueryJSON type.
     */
    find(dbID, collectionName, query) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.FindRequest();
            req.setDbid(dbID);
            req.setCollectionname(collectionName);
            // @todo: Find a more isomorphic way to do this base64 round-trip
            req.setQueryjson(Buffer.from(JSON.stringify(query)).toString('base64'));
            const res = (yield this.unary(api_pb_service_1.API.Find, req));
            const ret = {
                instancesList: res.instancesList.map((instance) => JSON.parse(Buffer.from(instance, 'base64').toString())),
            };
            return ret;
        });
    }
    /**
     * findByID queries the store for the id of an instance.
     * @param dbID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     * @param ID The id of the instance to search for.
     */
    findByID(dbID, collectionName, ID) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new api_pb_1.FindByIDRequest();
            req.setDbid(dbID);
            req.setCollectionname(collectionName);
            req.setInstanceid(ID);
            const res = (yield this.unary(api_pb_service_1.API.FindByID, req));
            const ret = {
                instance: JSON.parse(Buffer.from(res.instance, 'base64').toString()),
            };
            return ret;
        });
    }
    /**
     * readTransaction creates a new read-only transaction object. See ReadTransaction for details.
     * @param dbID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     */
    readTransaction(dbID, collectionName) {
        const client = grpc_web_1.grpc.client(api_pb_service_1.API.ReadTransaction, {
            host: this.config.host,
        });
        return new models_1.ReadTransaction(this.config, client, dbID, collectionName);
    }
    /**
     * writeTransaction creates a new writeable transaction object. See WriteTransaction for details.
     * @param dbID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     */
    writeTransaction(dbID, collectionName) {
        const client = grpc_web_1.grpc.client(api_pb_service_1.API.WriteTransaction, {
            host: this.config.host,
        });
        return new models_1.WriteTransaction(this.config, client, dbID, collectionName);
    }
    /**
     * listen opens a long-lived connection with a remote node, running the given callback on each new update to the given instance.
     * The return value is a `close` function, which cleanly closes the connection with the remote node.
     * @param dbID the ID of the database
     * @param filters contains an array of Filters
     * @param callback The callback to call on each update to the given instance.
     */
    listen(dbID, filters, callback) {
        const req = new api_pb_1.ListenRequest();
        req.setDbid(dbID);
        for (const filter of filters) {
            const requestFilter = new api_pb_1.ListenRequest.Filter();
            if (filter.instanceID) {
                requestFilter.setInstanceid(filter.instanceID);
            }
            else if (filter.collectionName) {
                requestFilter.setCollectionname(filter.collectionName);
            }
            if (filter.actionTypes) {
                for (const at of filter.actionTypes) {
                    switch (at) {
                        case 'ALL': {
                            requestFilter.setAction(0);
                        }
                        case 'CREATE': {
                            requestFilter.setAction(1);
                        }
                        case 'SAVE': {
                            requestFilter.setAction(2);
                        }
                        case 'DELETE': {
                            requestFilter.setAction(3);
                        }
                    }
                }
            }
            else {
                requestFilter.setAction(0);
            }
            req.addFilters(requestFilter);
        }
        const res = grpc_web_1.grpc.invoke(api_pb_service_1.API.Listen, {
            host: this.config.host,
            request: req,
            metadata: this.config._wrapMetadata(),
            onMessage: (rec) => {
                const ret = {
                    instance: JSON.parse(Buffer.from(rec.getInstance_asU8()).toString()),
                };
                callback(ret);
            },
            onEnd: (status, message, _trailers) => {
                if (status !== grpc_web_1.grpc.Code.OK) {
                    return callback(undefined, new Error(message));
                }
                callback();
            },
        });
        return res.close.bind(res);
    }
    unary(methodDescriptor, req) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => {
                grpc_web_1.grpc.unary(methodDescriptor, {
                    request: req,
                    host: this.config.host,
                    metadata: this.config._wrapMetadata(),
                    onEnd: (res) => {
                        const { status, statusMessage, message } = res;
                        if (status === grpc_web_1.grpc.Code.OK) {
                            if (message) {
                                resolve(message.toObject());
                            }
                            else {
                                resolve();
                            }
                        }
                        else {
                            reject(new Error(statusMessage));
                        }
                    },
                });
            });
        });
    }
}
exports.Client = Client;
// eslint-disable-next-line import/no-default-export
exports.default = Client;
