"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());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = exports.ReadTransaction = exports.WriteTransaction = exports.Where = exports.Query = void 0;
/**
 * @packageDocumentation
 * @module @textile/threads-client
 */
const grpc_web_1 = require("@improbable-eng/grpc-web");
const threads_pb_service_1 = require("@textile/threads-client-grpc/threads_pb_service");
const pb = __importStar(require("@textile/threads-client-grpc/threads_pb"));
const next_tick_1 = __importDefault(require("next-tick"));
const threads_core_1 = require("@textile/threads-core");
const multiaddr_1 = require("@textile/multiaddr");
const threads_id_1 = require("@textile/threads-id");
const to_json_schema_1 = __importDefault(require("to-json-schema"));
const context_1 = require("@textile/context");
const bs58_1 = require("bs58");
const models_1 = require("./models");
Object.defineProperty(exports, "Query", { enumerable: true, get: function () { return models_1.Query; } });
Object.defineProperty(exports, "Where", { enumerable: true, get: function () { return models_1.Where; } });
Object.defineProperty(exports, "WriteTransaction", { enumerable: true, get: function () { return models_1.WriteTransaction; } });
Object.defineProperty(exports, "ReadTransaction", { enumerable: true, get: function () { return models_1.ReadTransaction; } });
/**
 * 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 {
    /**
     * Creates a new gRPC client instance for accessing the Textile Threads API.
     * @param context The context to use for interacting with the APIs. Can be modified later.
     */
    constructor(context = new context_1.Context()) {
        this.context = context;
        this.serviceHost = context.host;
        this.rpcOptions = {
            transport: context.transport,
            debug: context.debug,
        };
        // If we have a default here, use it. Otherwise, rely on specific calls
        this.rpcOptions.transport && grpc_web_1.grpc.setDefaultTransport(this.rpcOptions.transport);
    }
    /**
     * Create a new gRPC client instance from a supplied user auth object.
     * Assumes all default gRPC setttings. For custimization options, use a context object directly.
     * @param auth The user auth object.
     * @example
     * ```typescript
     * import {UserAuth, Client} from '@textile/threads'
     *
     * async function create (auth: UserAuth) {
     *   return await Client.withUserAuth(auth)
     * }
     * ```
     */
    static withUserAuth(auth, host = context_1.defaultHost, debug = false) {
        const context = context_1.Context.fromUserAuth(auth, host, debug);
        return new Client(context);
    }
    /**
     * Create a new gRPC client instance from a supplied key and secret
     * @param key The KeyInfo object containing {key: string, secret: string, type: 0}. 0 === User Group Key, 1 === Account Key
     * @example
     * ```typescript
     * import {KeyInfo, Client} from '@textile/threads'
     *
     * async function create (keyInfo: KeyInfo) {
     *   return await Client.withKeyInfo(keyInfo)
     * }
     * ```
     */
    static withKeyInfo(key, host = context_1.defaultHost, debug = false) {
        return __awaiter(this, void 0, void 0, function* () {
            const context = new context_1.Context(host, debug);
            yield context.withKeyInfo(key);
            return new Client(context);
        });
    }
    /**
     * Create a random user identity.
     * @example
     * ```typescript
     * import {Client} from '@textile/threads'
     *
     * async function newIdentity () {
     *   const user = await Client.randomIdentity()
     *   return user
     * }
     * ```
     */
    static randomIdentity() {
        return __awaiter(this, void 0, void 0, function* () {
            return threads_core_1.Libp2pCryptoIdentity.fromRandom();
        });
    }
    /**
     * Obtain a token for interacting with the remote API.
     * @param identity A user identity to use for creating records in the database. A random identity
     * can be created with `Client.randomIdentity(), however, it is not easy/possible to migrate
     * identities after the fact. Please store or otherwise persist any identity information if
     * you wish to retrieve user data later, or use an external identity provider.
     * @param ctx Context object containing web-gRPC headers and settings.
     * @example
     * ```typescript
     * import {Client, Identity} from '@textile/threads'
     *
     * async function newToken (client: Client, user: Identity) {
     *   // Token is added to the client connection at the same time
     *   const token = await client.getToken(user)
     *   return token
     * }
     * ```
     */
    getToken(identity, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.getTokenChallenge(identity.public.toString(), (challenge) => __awaiter(this, void 0, void 0, function* () {
                return identity.sign(challenge);
            }), ctx);
        });
    }
    /**
     * Obtain a token for interacting with the remote API.
     * @param publicKey The public key of a user identity to use for creating records in the database.
     * A random identity can be created with `Client.randomIdentity(), however, it is not
     * easy/possible to migrate identities after the fact. Please store or otherwise persist any
     * identity information if you wish to retrieve user data later, or use an external identity
     * provider.
     * @param callback A callback function that takes a `challenge` argument and returns a signed
     * message using the input challenge and the private key associated with `publicKey`.
     * @param ctx Context object containing web-gRPC headers and settings.
     * @note `publicKey` must be the corresponding public key of the private key used in `callback`.
     */
    getTokenChallenge(publicKey, callback, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            const client = grpc_web_1.grpc.client(threads_pb_service_1.API.GetToken, {
                host: this.serviceHost,
                transport: this.rpcOptions.transport,
                debug: this.rpcOptions.debug,
            });
            return new Promise((resolve, reject) => {
                let token = '';
                client.onMessage((message) => __awaiter(this, void 0, void 0, function* () {
                    if (message.hasChallenge()) {
                        const challenge = message.getChallenge_asU8();
                        const signature = yield callback(challenge);
                        const req = new pb.GetTokenRequest();
                        req.setSignature(signature);
                        client.send(req);
                        client.finishSend();
                    }
                    else if (message.hasToken()) {
                        token = message.getToken();
                    }
                }));
                client.onEnd((code, message, _trailers) => {
                    client.close();
                    if (code === grpc_web_1.grpc.Code.OK) {
                        this.context.withToken(token);
                        resolve(token);
                    }
                    else {
                        reject(new Error(message));
                    }
                });
                const req = new pb.GetTokenRequest();
                req.setKey(publicKey);
                const metadata = Object.assign(Object.assign({}, this.context.toJSON()), ctx === null || ctx === void 0 ? void 0 : ctx.toJSON());
                client.start(metadata);
                client.send(req);
            });
        });
    }
    /**
     * newDB creates a new store on the remote node.
     * @param threadID the ID of the database
     * @param name The human-readable name for the database
     * @example
     * ```typescript
     * import {Client, ThreadID} from '@textile/threads'
     *
     * async function createDB (client: Client) {
     *   const thread: ThreadID = await client.newDB()
     *   return thread
     * }
     * ```
     */
    newDB(threadID, name) {
        return __awaiter(this, void 0, void 0, function* () {
            const dbID = threadID !== null && threadID !== void 0 ? threadID : threads_id_1.ThreadID.fromRandom();
            const req = new pb.NewDBRequest();
            req.setDbid(dbID.toBytes());
            if (name !== undefined) {
                this.context.withThreadName(name);
                req.setName(name);
            }
            yield this.unary(threads_pb_service_1.API.NewDB, req);
            // Update our context with out new thread id
            this.context.withThread(dbID.toString());
            return dbID;
        });
    }
    /**
     * Deletes an entire DB.
     * @param threadID the ID of the database.
     * @example
     * ```typescript
     * import {Client, ThreadID} from '@textile/threads'
     *
     * async function deleteDB (client: Client, thread: ThreadID) {
     *   await client.deleteDB(thread)
     *   return
     * }
     * ```
     */
    deleteDB(threadID) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.DeleteDBRequest();
            req.setDbid(threadID.toBytes());
            yield this.unary(threads_pb_service_1.API.DeleteDB, req);
            return;
        });
    }
    /**
     * Lists all known DBs.
     */
    listDBs() {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.ListDBsRequest();
            const res = (yield this.unary(threads_pb_service_1.API.ListDBs, req));
            const dbs = {};
            for (const db of res.dbsList) {
                const id = threads_id_1.ThreadID.fromBytes(Buffer.from(db.dbid, 'base64')).toString();
                dbs[id] = db.info;
            }
            return dbs;
        });
    }
    /**
     * newCollection registers a new collection schema under the given name.
     * The schema must be a valid json-schema.org schema, and can be a JSON string or object.
     * @param threadID the ID of the database
     * @param name The human-readable name for the collection.
     * @param schema The actual json-schema.org compatible schema object.
     * @param indexes A set of index definitions for indexing instance fields.
     */
    newCollection(threadID, name, schema, indexes) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.NewCollectionRequest();
            const config = new pb.CollectionConfig();
            config.setName(name);
            config.setSchema(Buffer.from(JSON.stringify(schema)));
            const idx = [];
            for (const item of indexes !== null && indexes !== void 0 ? indexes : []) {
                const index = new pb.Index();
                index.setPath(item.path);
                index.setUnique(item.unique);
                idx.push(index);
            }
            config.setIndexesList(idx);
            req.setDbid(threadID.toBytes());
            req.setConfig(config);
            yield this.unary(threads_pb_service_1.API.NewCollection, req);
            return;
        });
    }
    /**
     * newCollectionFromObject creates and registers a new collection under the given name.
     * The input object must be serializable to JSON, and contain only json-schema.org types.
     * @param threadID the ID of the database
     * @param name The human-readable name for the collection.
     * @param obj The actual object to attempt to extract a schema from.
     * @param indexes A set of index definitions for indexing instance fields.
     * @example
     * ```typescript
     * import {Client, ThreadID} from '@textile/threads'
     *
     * async function fromObject (client: Client, thread: ThreadID, name: string, obj: any) {
     *   await client.newCollectionFromObject(thread, name, obj)
     *   return
     * }
     *
     * // Example object
     * // const person = {name: 'Buzz', missions: 3}
     * ```
     */
    newCollectionFromObject(threadID, name, obj, indexes) {
        return __awaiter(this, void 0, void 0, function* () {
            const schema = to_json_schema_1.default(obj);
            return this.newCollection(threadID, name, schema, indexes);
        });
    }
    /**
     * updateCollection updates an existing collection.
     * Currenrly, updates can include name and schema.
     * @todo Allow update of indexing information.
     * @param threadID the ID of the database
     * @param name The human-readable name for the collection.
     * @param config The new collection configuration values to use when updating.
     */
    updateCollection(threadID, name, schema, indexes) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.UpdateCollectionRequest();
            const conf = new pb.CollectionConfig();
            conf.setName(name);
            conf.setSchema(Buffer.from(JSON.stringify(schema)));
            const idx = [];
            for (const item of indexes !== null && indexes !== void 0 ? indexes : []) {
                const index = new pb.Index();
                index.setPath(item.path);
                index.setUnique(item.unique);
                idx.push(index);
            }
            conf.setIndexesList(idx);
            req.setDbid(threadID.toBytes());
            req.setConfig(conf);
            yield this.unary(threads_pb_service_1.API.UpdateCollection, req);
            return;
        });
    }
    /**
     * deleteCollection deletes an existing collection.
     * @param threadID the ID of the database.
     * @param name The human-readable name for the collection.
     * @param schema The actual json-schema.org compatible schema object.
     * @example
     * ```typescript
     * import {Client, ThreadID} from '@textile/threads'
     *
     * async function deleteCollection (client: Client, thread: ThreadID, name: string) {
     *   await client.deleteCollection(thread, name)
     *   return
     * }
     *
     * // Example object
     * // const person = {name: 'Buzz', missions: 3}
     * ```
     */
    deleteCollection(threadID, name) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.DeleteCollectionRequest();
            req.setDbid(threadID.toBytes());
            req.setName(name);
            yield this.unary(threads_pb_service_1.API.DeleteCollection, req);
            return;
        });
    }
    /**
     * getCollectionIndexes returns an existing collection's indexes.
     * @param threadID the ID of the database.
     * @param name The human-readable name for the collection.
     */
    getCollectionIndexes(threadID, name) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.GetCollectionIndexesRequest();
            req.setDbid(threadID.toBytes());
            req.setName(name);
            const res = (yield this.unary(threads_pb_service_1.API.GetCollectionIndexes, req));
            return res.indexesList;
        });
    }
    /**
     * 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 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 key The set of keys to use to connect to the database
     * @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 pb.NewDBFromAddrRequest();
            const addr = new multiaddr_1.Multiaddr(address).buffer;
            req.setAddr(addr);
            req.setKey(typeof key === 'string' ? bs58_1.decode(key) : key);
            req.setCollectionsList(collections.map((c) => {
                const config = new pb.CollectionConfig();
                config.setName(c.name);
                config.setSchema(JSON.stringify(c.schema));
                return config;
            }));
            return yield this.unary(threads_pb_service_1.API.NewDBFromAddr, req);
        });
    }
    /**
     * getDBInfo returns invite 'links' unseful for inviting other peers to join a given store/thread.
     * @param threadID the ID of the database
     */
    getDBInfo(threadID) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.GetDBInfoRequest();
            req.setDbid(threadID.toBytes());
            const res = (yield this.unary(threads_pb_service_1.API.GetDBInfo, req));
            const invites = [];
            for (const addr of res.addrsList) {
                const dk = Buffer.from(res.key, 'base64');
                const a = typeof addr === 'string' ? Buffer.from(addr, 'base64') : Buffer.from(addr);
                const address = new multiaddr_1.Multiaddr(a).toString();
                invites.push({
                    address,
                    key: bs58_1.encode(dk),
                });
            }
            return invites;
        });
    }
    /**
     * Creates a new model instance in the given store.
     * @param threadID 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(threadID, collectionName, values) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.CreateRequest();
            req.setDbid(threadID.toBytes());
            req.setCollectionname(collectionName);
            const list = [];
            values.forEach((v) => {
                list.push(Buffer.from(JSON.stringify(v)));
            });
            req.setInstancesList(list);
            const res = (yield this.unary(threads_pb_service_1.API.Create, req));
            return res.instanceidsList;
        });
    }
    /**
     * Saves changes to an existing model instance in the given store.
     * @param threadID 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(threadID, collectionName, values) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.SaveRequest();
            req.setDbid(threadID.toBytes());
            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(threads_pb_service_1.API.Save, req);
            return;
        });
    }
    /**
     * Deletes an existing model instance from the given store.
     * @param threadID 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(threadID, collectionName, IDs) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.DeleteRequest();
            req.setDbid(threadID.toBytes());
            req.setCollectionname(collectionName);
            req.setInstanceidsList(IDs);
            yield this.unary(threads_pb_service_1.API.Delete, req);
            return;
        });
    }
    /**
     * has checks whether a given instance exists in the given store.
     * @param threadID 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(threadID, collectionName, IDs) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.HasRequest();
            req.setDbid(threadID.toBytes());
            req.setCollectionname(collectionName);
            req.setInstanceidsList(IDs);
            const res = (yield this.unary(threads_pb_service_1.API.Has, req));
            return res.exists;
        });
    }
    /**
     * find queries the store for entities matching the given query parameters.
     * @param threadID 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(threadID, collectionName, query) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.FindRequest();
            req.setDbid(threadID.toBytes());
            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(threads_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 threadID 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(threadID, collectionName, ID) {
        return __awaiter(this, void 0, void 0, function* () {
            const req = new pb.FindByIDRequest();
            req.setDbid(threadID.toBytes());
            req.setCollectionname(collectionName);
            req.setInstanceid(ID);
            const res = (yield this.unary(threads_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 threadID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     */
    readTransaction(threadID, collectionName) {
        const client = grpc_web_1.grpc.client(threads_pb_service_1.API.ReadTransaction, {
            host: this.serviceHost,
            transport: this.rpcOptions.transport,
            debug: this.rpcOptions.debug,
        });
        return new models_1.ReadTransaction(this.context, client, threadID, collectionName);
    }
    /**
     * writeTransaction creates a new writeable transaction object. See WriteTransaction for details.
     * @param threadID the ID of the database
     * @param collectionName The human-readable name of the model to use.
     */
    writeTransaction(threadID, collectionName) {
        const client = grpc_web_1.grpc.client(threads_pb_service_1.API.WriteTransaction, {
            host: this.serviceHost,
            transport: this.rpcOptions.transport,
            debug: this.rpcOptions.debug,
        });
        return new models_1.WriteTransaction(this.context, client, threadID, 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 threadID 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(threadID, filters, callback) {
        const req = new pb.ListenRequest();
        req.setDbid(threadID.toBytes());
        for (const filter of filters) {
            const requestFilter = new pb.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(pb.ListenRequest.Filter.Action.ALL);
                            break;
                        }
                        case 'CREATE': {
                            requestFilter.setAction(pb.ListenRequest.Filter.Action.CREATE);
                            break;
                        }
                        case 'SAVE': {
                            requestFilter.setAction(pb.ListenRequest.Filter.Action.SAVE);
                            break;
                        }
                        case 'DELETE': {
                            requestFilter.setAction(pb.ListenRequest.Filter.Action.DELETE);
                            break;
                        }
                    }
                }
            }
            else {
                requestFilter.setAction(0);
            }
            req.addFilters(requestFilter);
        }
        const metadata = JSON.parse(JSON.stringify(this.context));
        const res = grpc_web_1.grpc.invoke(threads_pb_service_1.API.Listen, {
            host: this.serviceHost,
            transport: this.rpcOptions.transport,
            debug: this.rpcOptions.debug,
            request: req,
            metadata,
            onMessage: (rec) => {
                const ret = {
                    instance: JSON.parse(Buffer.from(rec.getInstance_asU8()).toString()),
                };
                next_tick_1.default(() => callback(ret));
            },
            onEnd: (status, message, _trailers) => {
                if (status !== grpc_web_1.grpc.Code.OK) {
                    next_tick_1.default(() => callback(undefined, new Error(message)));
                }
                next_tick_1.default(callback);
            },
        });
        return res.close.bind(res);
    }
    unary(methodDescriptor, req) {
        return __awaiter(this, void 0, void 0, function* () {
            const metadata = JSON.parse(JSON.stringify(this.context));
            return new Promise((resolve, reject) => {
                grpc_web_1.grpc.unary(methodDescriptor, {
                    transport: this.rpcOptions.transport,
                    debug: this.rpcOptions.debug,
                    request: req,
                    host: this.serviceHost,
                    metadata,
                    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;
//# sourceMappingURL=index.js.map