/** * @packageDocumentation * @module @textile/threads-client */ import { grpc } from '@improbable-eng/grpc-web'; import { ContextInterface } from '@textile/context'; import { Identity } from '@textile/crypto'; import { KeyInfo, UserAuth } from '@textile/security'; import * as pb from '@textile/threads-client-grpc/threads_pb'; import { ThreadID } from '@textile/threads-id'; import 'fastestsmallesttextencoderdecoder'; import { JSONSchema3or4 } from 'to-json-schema'; import { ComparisonJSON, CriterionJSON, Event, Filter, Patch, PatchType, Query, QueryJSON, ReadTransaction, SortJSON, ValueJSON, Where, WriteTransaction } from './models'; export { Filter, Query, Where, WriteTransaction, ReadTransaction, QueryJSON, ValueJSON, ComparisonJSON, CriterionJSON, SortJSON, JSONSchema3or4, Event, Patch, PatchType, }; export declare function getFunctionBody(fn: ((...args: any[]) => any) | string): string; /** * CollectionConfig is the configuration options for creating and updating a Collection. * It supports the following configuration options: * - Name: The name of the collection, e.g, "Animals" (must be unique per DB). * - Schema: A JSON Schema), which is used for instance validation. * - Indexes: An optional list of index configurations, which define how instances are indexed. * - WriteValidator: An optional JavaScript (ECMAScript 5.1) function that is used to validate * instances on write. * - ReadFilter: An optional JavaScript (ECMAScript 5.1) function that is used to filter * instances on read. * * The `writeValidator` function receives three arguments: * - writer: The multibase-encoded public key identity of the writer. * - event: An object describing the update event (see core.Event). * - instance: The current instance as a JavaScript object before the update event is applied. * * A falsy return value indicates a failed validation. Note that the function arguments must * be named as documented here (writer, event, instance). These functions run in a secure sandbox * where these argument names are specified. * * Having access to writer, event, and instance opens the door to a variety of app-specific logic. * Textile Buckets file-level access roles are implemented in part with a write validator. * * The `readFilter` function receives three arguments: * - reader: The multibase-encoded public key identity of the reader. * - instance: The current instance as a JavaScript object. * * The function must return a JavaScript object. Most implementations will modify and return the * current instance. Note that the function arguments must be named as documented here (reader, * instance). These functions run in a secure sandbox where these argument names are specified. * Like write validation, read filtering opens the door to a variety of app-specific logic. * Textile Buckets file-level access roles are implemented in part with a read filter. */ export interface CollectionConfig { name: string; schema?: JSONSchema3or4 | any; indexes?: pb.Index.AsObject[]; writeValidator?: ((writer: string, event: Event, instance: W) => boolean) | string; readFilter?: ((reader: string, instance: R) => R) | string; } export declare function maybeLocalAddr(ip: string): boolean | RegExpMatchArray; export declare enum Action { CREATE = 0, SAVE = 1, DELETE = 2 } export interface Update { instance: T | undefined; collectionName: string; instanceID: string; action: Action; } /** * DBInfo contains joining/sharing information for a Thread/DB. */ export interface DBInfo { /** * The Thread Key, encoded as a base32 string. * @see {@link ThreadKey} for details. */ key: string; /** * The Multiaddrs for a peer hosting the given Thread/DB. */ addrs: string[]; } /** * Client is a web-gRPC wrapper client for communicating with a webgRPC-enabled Threads server. * This client library can be used to interact with a local or remote Textile gRPC-service * It is a wrapper around Textile Thread's 'DB' API, which is defined here: * https://github.com/textileio/go-threads/blob/master/api/pb/api.proto. * * @example * ```typescript * import {Client, Identity, UserAuth} from '@textile/threads' * * async function setupDB(auth: UserAuth, identity: Identity) { * // Initialize the client * const client = Client.withUserAuth(auth) * * // Connect the user to your API * const userToken = await client.getToken(identity) * * // Create a new DB * const threadID = await client.newDB(undefined, 'nasa') * * // Create a new Collection from an Object * const buzz = { * name: 'Buzz', * missions: 2, * _id: '', * } * await client.newCollectionFromObject(threadID, buzz, { name: 'astronauts' }) * * // Store the buzz object in the new collection * await client.create(threadID, 'astronauts', [buzz]) * * return threadID * } * ``` */ export declare class Client { context: ContextInterface; serviceHost: string; rpcOptions: grpc.RpcOptions; /** * 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. * @param debug Should we run in debug mode. Defaults to false. */ constructor(context?: ContextInterface, debug?: boolean); /** * Create a new gRPC client instance from a supplied user auth object. * Assumes all default gRPC settlings. For customization options, use a context object directly. * The callback method will automatically refresh expiring credentials. * @param auth The user auth object or an async callback that returns a user auth object. * @example * ```typescript * import {UserAuth, Client} from '@textile/threads' * * function create (auth: UserAuth) { * return Client.withUserAuth(auth) * } * ``` * @example * ```typescript * import {UserAuth, Client} from '@textile/threads' * * function setCallback (callback: () => Promise) { * return Client.withUserAuth(callback) * } * ``` */ static withUserAuth(auth: UserAuth | (() => Promise), host?: string, debug?: boolean): Client; /** * 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 * @param host The remote gRPC host to connect with. Should be left as default. * @param debug Whether to run in debug mode. Defaults to false. * @example * ```typescript * import {KeyInfo, Client} from '@textile/threads' * * async function create (keyInfo: KeyInfo) { * return await Client.withKeyInfo(keyInfo) * } * ``` */ static withKeyInfo(key: KeyInfo, host?: string, debug?: boolean): Promise; /** * Obtain a token per user (identity) 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: Identity, ctx?: ContextInterface): Promise; /** * Obtain a token per user (identity) 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. * @remarks `publicKey` must be the corresponding public key of the private key used in `callback`. */ getTokenChallenge(publicKey: string, callback: (challenge: Uint8Array) => Uint8Array | Promise, ctx?: ContextInterface): Promise; /** * 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 threadID: ThreadID = await client.newDB() * return threadID * } * ``` */ newDB(threadID?: ThreadID, name?: string): Promise; /** * open creates and enters 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 openDB (client: Client, threadID: ThreadID) { * await client.open(threadID) * } * ``` */ open(threadID: ThreadID, name?: string): Promise; /** * Deletes an entire DB. * @param threadID the ID of the database. * @example * ```typescript * import {Client, ThreadID} from '@textile/threads' * * async function deleteDB (client: Client, threadID: ThreadID) { * await client.deleteDB(threadID) * return * } * ``` */ deleteDB(threadID: ThreadID): Promise; /** * Lists all known DBs. */ listDBs(): Promise>; /** * Lists the collections in a thread * @param thread the ID of the database */ listCollections(thread: ThreadID): Promise>; /** * 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 config A configuration object for the collection. See {@link CollectionConfig}. Note * that the validator and filter functions can also be provided as strings. * * @example * Create a new astronauts collection * ```typescript * import {Client, ThreadID} from '@textile/threads' * * const astronauts = { * title: "Astronauts", * type: "object", * required: ["_id"], * properties: { * _id: { * type: "string", * description: "The instance's id.", * }, * name: { * type: "string", * description: "The astronauts name.", * }, * missions: { * description: "The number of missions.", * type: "integer", * minimum: 0, * }, * }, * } * * async function newCollection (client: Client, threadID: ThreadID) { * return await client.updateCollection(threadID, { name: 'astronauts', schema: astronauts }) * } * ``` * @example * Create a collection with writeValidator and readFilter functions * ```typescript * import {Client, ThreadID} from '@textile/threads' * * const schema = { * title: "Person", * type: "object", * required: ["_id"], * properties: { * _id: { type: "string" }, * name: { type: "string" }, * age: { type: "integer" }, * }, * } * * // We'll create a helper interface for type-safety * interface Person { * _id: string * age: number * name: string * } * * const writeValidator = (writer: string, event: any, instance: Person) => { * var type = event.patch.type * var patch = event.patch.json_patch * switch (type) { * case "delete": * if (writer != "the_boss") { * return false // Not the boss? No deletes for you. * } * default: * return true * } * } * * const readFilter = (reader: string, instance: Person) => { * if (instance.age > 50) { * delete instance.age // Let's just hide their age for them ;) * } * return instance * } * * async function newCollection (client: Client, threadID: ThreadID) { * return await client.updateCollection(threadID, { * name: 'Person', schema, writeValidator, readFilter * }) * } * ``` */ newCollection(threadID: ThreadID, config: CollectionConfig): Promise; /** * 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 obj The actual object to attempt to extract a schema from. * @param config A configuration object for the collection. See {@link CollectionConfig}. * * @example * Change a new astronauts collection based of Buzz * ```typescript * import {Client, ThreadID} from '@textile/threads' * * async function newCollection (client: Client, threadID: ThreadID) { * const buzz = { * name: 'Buzz', * missions: 2, * _id: '', * } * return await client.newCollectionFromObject(threadID, buzz, { name: 'astronauts' }) * } * ``` */ newCollectionFromObject(threadID: ThreadID, obj: Record, config: Omit): Promise; /** * updateCollection updates an existing collection. * Currently, updates can include name and schema. * @todo Allow update of indexing information. * @param threadID the ID of the database * @param config A configuration object for the collection. See {@link CollectionConfig}. * * @example * Change the name of our astronauts collection * ```typescript * import {Client, ThreadID} from '@textile/threads' * * const astronauts = { * title: "Astronauts", * type: "object", * required: ["_id"], * properties: { * _id: { * type: "string", * description: "The instance's id.", * }, * name: { * type: "string", * description: "The astronauts name.", * }, * missions: { * description: "The number of missions.", * type: "integer", * minimum: 0, * }, * }, * } * * async function changeName (client: Client, threadID: ThreadID) { * return await client.updateCollection(threadID, { name: 'toy-story-characters', schema: astronauts }) * } * ``` */ updateCollection(threadID: ThreadID, config: CollectionConfig): Promise; /** * 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 deleteAstronauts (client: Client, thread: ThreadID) { * await client.deleteCollection(thread, 'astronauts') * return * } * ``` */ deleteCollection(threadID: ThreadID, name: string): Promise; /** * Returns an existing indexes for a collection. * @param threadID the ID of the database. * @param name The human-readable name for the collection. * * @example * Return a set of indexes for our astronauts collection * ```typescript * import {Client, ThreadID} from '@textile/threads' * * async function getIndexes (client: Client, threadID: ThreadID) { * return await client.getCollectionIndexes(threadID, 'astronauts') * } * ``` */ getCollectionIndexes(threadID: ThreadID, name: string): Promise; getCollectionInfo(threadID: ThreadID, name: string): Promise; /** * 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/follow key, which should be a Uint8Array or base32-encoded string. * @remarks * See getDBInfo for a possible source of the address and keys. See {@link ThreadKey} for * information about thread keys. * @param address The address for the thread with which to connect. * Should be of the form /ip4//tcp//p2p//thread/ * @param key The set of keys to use to connect to the database * @param collections Array of CollectionConfig objects for seeding the DB with collections. */ newDBFromAddr(address: string, key: string | Uint8Array, collections?: Array): Promise; /** * Connect client to an existing database using information in the DBInfo object * This should be called before any operation on the store, and is an alternative * to open, which re-opens a database already opened by the user. * @remarks This is a helper method around newDBFromAddr, which takes the 'raw' output * from getDBInfo. See getDBInfo for a possible source of the address and keys. * @param info The output from a call to getDBInfo on a separate peer. * @param includeLocal Whether to try dialing addresses that appear to be on the local host. * Defaults to false, preferring to add from public ip addresses. * @param collections Array of `name` and JSON schema pairs for seeding the DB with collections. * * @example * Get DB info and use DB info to join an existing remote thread (e.g. invited) * ```typescript * import {Client, DBInfo, ThreadID} from '@textile/threads' * * async function getInfo (client: Client, threadID: ThreadID): Promise { * return await client.getDBInfo(threadID) * } * * async function joinFromInfo (client: Client, info: DBInfo) { * return await client.joinFromInfo(info) * } * ``` */ joinFromInfo(info: DBInfo, includeLocal?: boolean, collections?: Array): Promise; /** * Returns a DBInfo objection containing metadata required to invite other peers to join a given thread. * @param threadID the ID of the database * @returns An object with an encoded thread key, and a list of multiaddrs. * * @example * Get DB info and use DB info to join an existing remote thread (e.g. invited) * ```typescript * import {Client, DBInfo, ThreadID} from '@textile/threads' * * async function getInfo (client: Client, threadID: ThreadID): Promise { * return await client.getDBInfo(threadID) * } * * async function joinFromInfo (client: Client, info: DBInfo) { * return await client.joinFromInfo(info) * } * ``` */ getDBInfo(threadID: ThreadID): Promise; /** * 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. * * @example * Create a new entry in our collection * ```typescript * import {Client, ThreadID, Where} from '@textile/threads' * * interface Astronaut { * name: string * missions: number * _id: string * } * * async function createBuzz (client: Client, threadID: ThreadID) { * const buzz: Astronaut = { * name: 'Buzz', * missions: 2, * _id: '', * } * * await client.create(threadID, 'astronauts', [buzz]) * } * ``` */ create(threadID: ThreadID, collectionName: string, values: any[]): Promise; /** * 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. * * @example * Update an existing instance * ```typescript * import {Client, ThreadID, Where} from '@textile/threads' * * interface Astronaut { * name: string * missions: number * _id: string * } * * async function updateBuzz (client: Client, threadID: ThreadID) { * const query = new Where('name').eq('Buzz') * const result = await client.find(threadID, 'astronauts', query) * * if (result.length < 1) return * * const buzz = result[0] * buzz.missions += 1 * * return await client.save(threadID, 'astronauts', [buzz]) * } * ``` */ save(threadID: ThreadID, collectionName: string, values: any[]): Promise; /** * 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. * * @example * Delete any instances that return from a query * ```typescript * import {Client, ThreadID, Where} from '@textile/threads' * * interface Astronaut { * name: string * missions: number * _id: string * } * * async function deleteBuzz (client: Client, threadID: ThreadID) { * const query = new Where('name').eq('Buzz') * const result = await client.find(threadID, 'astronauts', query) * * if (result.length < 1) return * * const ids = await result.map((instance) => instance._id) * await client.delete(threadID, 'astronauts', ids) * } * ``` */ delete(threadID: ThreadID, collectionName: string, IDs: string[]): Promise; /** * Check if a given instance exists in the collection. * @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. * * @example * Check if an instance exists * ```typescript * import {Client, ThreadID, Where} from '@textile/threads' * * async function instanceExists (client: Client, threadID: ThreadID, id: string) { * return await client.has(threadID, 'astronauts', [id]) * } * ``` */ has(threadID: ThreadID, collectionName: string, IDs: string[]): Promise; /** * Queries a collection 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. * * @example * Query with return type * ```typescript * import {Client, ThreadID, Where} from '@textile/threads' * * interface Astronaut { * name: string * missions: number * _id: string * } * * async function getAstronautByName (client: Client, threadID: ThreadID, name: string) { * const query = new Where('name').eq(name) * const astronaut = await client.find(threadID, 'astronauts', query) * return astronaut * } * ``` */ find(threadID: ThreadID, collectionName: string, query: QueryJSON): Promise; /** * Queries the collection by a known instance ID. * @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. * * @example * Find and cast a known model by instance ID. * ```typescript * import {Client, ThreadID} from '@textile/threads' * * interface Astronaut { * name: string * missions: number * _id: string * } * * async function getAstronaut (client: Client, threadID: ThreadID, id: string) { * const astronaut = await client.findByID(threadID, 'astronauts', id) * return astronaut * } * ``` * * @example * Simple find and return any instance * ```typescript * import {Client, ThreadID} from '@textile/threads' * * async function getInstance (client: Client, threadID: ThreadID, id: string) { * return await client.findByID(threadID, 'astronauts', id) * } * ``` */ findByID(threadID: ThreadID, collectionName: string, ID: string): Promise; /** * Verify checks existing instance changes. * Each model instance must have a valid existing `_id` property. * @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. * * @example * Update an existing instance * ```typescript * import {Client, ThreadID, Where} from '@textile/threads' * * interface Astronaut { * name: string * missions: number * _id: string * } * * async function verifyBuzz (client: Client, threadID: ThreadID) { * const query = new Where('name').eq('Buzz') * const result = await client.find(threadID, 'astronauts', query) * * if (result.length < 1) return * * const buzz = result[0] * buzz.missions += 1 * * // Is this going to be a valid update? * return await client.verify(threadID, 'astronauts', [buzz]) * } * ``` */ verify(threadID: ThreadID, collectionName: string, values: any[]): Promise; /** * 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: ThreadID, collectionName: string): ReadTransaction; /** * 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: ThreadID, collectionName: string): WriteTransaction; /** * 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. * * @example * ```typescript * import {Client, ThreadID, Update} from '@textile/threads' * * interface Astronaut { * name: string * missions: number * _id: string * } * function setupListener (client: Client, threadID: ThreadID) { * const callback = (update?: Update) => { * // Not safe if more than the Astronauts collection existed in the same DB * if (!update || !update.instance) return * console.log('New update:', update.instance.name, update.instance.missions) * } * const closer = client.listen(threadID, [], callback) * return closer * } * ``` * * @example * Listen to only CREATE events on a specific Collection. * ```typescript * import {Client, ThreadID, Update} from '@textile/threads' * * interface Astronaut { * name: string * missions: number * _id: string * } * function setupListener (client: Client, threadID: ThreadID) { * const callback = (update?: Update) => { * if (!update || !update.instance) return * console.log('New update:', update.instance.name, update.instance.missions) * } * const filters = [ * {collectionName: 'Astronauts'}, * {actionTypes: ['CREATE']} * ] * const closer = client.listen(threadID, filters, callback) * return closer * } * ``` */ listen(threadID: ThreadID, filters: Filter[], callback: (reply?: Update, err?: Error) => void): grpc.Request; private unary; } export default Client;