Using the Client SDK

Thank you for checking out the Vendia Client SDK! We've been building and testing this client internally for months and enthusiastically encourage you to try it out! The project is still under active development, however, and the API is subject to change. Thanks for your patience!

The Client SDK is a type-safe TypeScript & JavaScript client for your Uni's API with auto-generated code customized to match your Uni's schema!

If you're new to Vendia and wondering what a "Uni" is? This is a great place to start: What is a Uni?.

The official Vendia client is the easiest way to start working with your Uni. Enjoy autocomplete (intellisense) in your favorite IDE, built-in support for both HTTP and websocket GraphQL APIs (see Realtime Data), multiple authentication methods, file upload/download, and additional conveniences. The client is isomorphic — it can be used in both the browser and server (node.js).

What does "auto-generated code" mean?

Code based on your Uni's schema will be generated automatically during installation (you can read more about how it works in the appendix below). If your schema included a "product" entity, for example, your generated client would include the following methods:

// List all the products
const listProductsResponse = await client.entities.product.list();

// Add a new product
const addProductResponse = await client.entities.product.add({
  name: 'super-widget',
  inventory: 100,
});

// Get a product by ID
const getProductResponse = await client.entities.product.get('abc-123');

Getting started

Prerequisites

  • Create a Vendia account and deploy a Uni you'd like to use!
  • Install the Vendia CLI (or update via npm update -g @vendia/share-cli)
npm install -g @vendia/share-cli

Step 1: Pulling your Uni’s schema

We’ll need to store some information about your Uni and its schema locally in order to generate custom, type-safe client code. Then we’ll use the Vendia CLI to authenticate into your Uni, fetch the required data, and store it locally.

Note: if you've already got a .vendia directory (e.g. another team member has already committed the .vendia directory and schema files to your repo), you can skip this step and continue to Step 2

  1. Navigate to the root directory of an existing project you’ve been working on or create a new, empty directory and cd into it.
  2. Use the following command and follow the prompts to fetch your Uni's schema data and store it locally:
share client:pull

If the command is successful, you should end up with a .vendia directory that looks like this:

.vendia
├── config.json
├── schema.graphql
└── schema.json

This directory can be committed to your repository and shared with others working on the same project.

Step 2: Installing the client

Use your favorite npm client to install the Vendia client package:

npm install @vendia/client

This is all you need to do.

Once the installation completes, a post-installation script will run automatically — this script generates custom TypeScript files (and compiles them to JavaScript and type declaration files). This is necessary for the client to work correctly!

If you installed the client before pulling your Uni's schema, just follow Step 1 above to pull with the CLI — you'll be prompted to run code generation afterwards.

You can also run just code generation with the following command:

share client:generate

Help! I got an error during npm installation!

Please take a look at fixes for common issues below!

Usage

Initializing the client

You can instantiate an instance of the Vendia client using the following code:

import { createVendiaClient } from '@vendia/client';

const client = createVendiaClient({
  apiUrl: `<Your GraphQL URL>`,
  websocketUrl: `<Your Websocket URL>`,
});

Options

  • apiUrl - string, required
  • websocketUrl - string, optional (but required in order to use GraphQL subscriptions)
  • apiKey - string, optional
  • debug - boolean, optional (set to true to enable verbose logging)

Authentication

The Vendia client currently only supports authentication via API key. More options for authentication coming soon!

API key

The easiest way to get started in a server-side scenario is with your API key — it can be passed in via the apiKey option when instantiating the client:

const client = createVendiaClient({
  apiUrl: `<Your GraphQL URL>`,
  websocketUrl: `<Your Websocket URL>`,
  apiKey: process.env.VENDIA_API_KEY, // <---- API key
});

Warning: Never expose your API key to untrusted users! API keys should only be used in server-side applications (node.js) and should always be accessed in code via environment variables.

Reading and writing data

Working with Entities

CRUD operations for each of the top-level data types (known as “entities”) defined in your Vendia JSON schema are available under the entities namespace. Entity names are converted to camelCase to conform with idiomatic JavaScript. For example, "CarParts" will be available at entities.carParts.

Let's assume your JSON schema had entities for “Product”, “Shipment”, and “User”. You could perform the following example operations:

const { entities } = client;

// Add a new "product"
const productResponse = await entities.product.add({
  name: 'super-widget',
  inventory: 100,
});

// List your "shipments"
const shipmentResponse = await entities.shipment.list();

// Get a "user" by id
const userResponse = await entities.user.get('abc-123');

Adding an item

Adding new items can be performed with the add method. This method takes a single argument: an object containing the data to be added. The add method returns a promise that resolves to the newly created item.

const response = await entities.product.add({
  name: 'super-widget',
  inventory: 100,
});

Singular entities — that is, entities that are defined in your JSON schema as any type other than "array" — must be created with the create method rather than add. Once you've created a singular entity, you can update it to make changes, but you can't add more than one.

const response = await entities.topSellingProductsSummary.create({
  topSellingProductName: 'super-widget',
  unitsSoldInLastNinetyDays: 100000,
});

Updating an item

Updating an item can be performed with the update method. This method has one required argument: an object containing the item to be updated which must include the existing item's _id. The update method returns a promise that resolves to the updated item.

A second, optional argument can be used to update an item conditionally.

const response = await entities.product.update({
  _id: existingProduct._id,
  name: 'EVEN-MORE-SUPER-widget',
  inventory: 1000000,
});

// Retrieving an item, changing a field, and saving the updated item
const product = await entities.product.get('abc-123');
product.inventory = product.inventory - 1;
const updateProductResponse = await entities.product.update(product)

Removing an item

Removing an item can be performed with the remove method. This method has a single required argument: the _id of the item to be removed. The remove method returns a promise that resolves to the removed item.

A second, optional argument can be used to remove an item conditionally.

const response = await entities.product.remove('abc-123');

Singular entities - that is, entities that are defined in your JSON schema as any type other than "array" - must be removed with the delete method rather than remove.

const response = await entities.topSellingProductsSummary.delete();

Conditionally updating or removing items

Update and removal mutations can be performed conditionally - that is, the update/removal will only be performed if the item currently stored in the database matches the provided conditions. Conditions are specified via an object passed to the condition option. Complex conditions can be specified by recursively nesting additional conditions via the _and/_or properties.

// Only perform this update if the item stored in the database has a name of "super-widget"
const updateResponse = await entities.product.update(
  {
    _id: existingProduct._id,
    name: 'EVEN-MORE-SUPER-widget',
    inventory: 1000000,
  },
  {
    condition: {
      name: {
        eq: 'super-widget',
      },
    },
  }
);

// Only remove this item if the version currently stored in the database has an inventory of 0 (or less) OR its name is prefixed with "OBSOLETE"
const removeResponse = await entities.product.remove('abc-123', {
  condition: {
    _or: {
      name: {
        beginsWith: 'OBSOLETE',
      },
      inventory: {
        le: 0,
      },
    },
  },
});

Listing items

Listing items can be performed with the list method. This method takes a single optional argument: an object containing options to be applied to the list request. The list method returns a promise that resolves to an array of items.

const response = await entities.product.list();

Pagination

Pagination is cursor-based — the list method will return a list of items along with a nextToken cursor that can be used to retrieve the next page of items - nextToken will be null when there are no more pages to retrieve.

const firstPage = await entities.product.list();

const secondPage = await entities.product.list({
  nextToken: firstPage.nextToken,
});

The number of items returned in each page can be controlled with the limit option (defaults to 50 items).

const fiveProductsResponse = await entities.product.list({ limit: 5 });

Filtering

List queries can be augmented with a powerful filtering syntax passed as an object to the filter option.

// List all products where name contains 'widget', inventory is greater than 50, and price is less than 100
const response = await entities.product.list({
  filter: {
    name: {
      contains: 'widget',
    },
    _and: {
      inventory: {
        gt: 50,
      },
      price: {
        lt: 100,
      },
    },
  },
});

Retrieving an item

Retrieving an item can be performed with the get method. This method has a single required argument: the _id of the item to be retrieved. The get method returns a promise that resolves to the retrieved item.

const response = await entities.product.get('abc-123');

Retrieving previous versions of an item

Previous versions can be retrieved by passing an optional second argument to the get method: an object containing the version of the item to be retrieved.

const response = await entities.product.get('abc-123', {
  version: 1,
});

Mutations are synchronous by default

Mutations will be performed synchronously, by default, meaning the promise returned from calling add/update/remove won’t resolve until the data has been safely stored in your Uni — this is probably the behavior you would expect! Keep in mind, however, that Vendia is a decentralized, ledgered database and consensus amongst all participating nodes in your Uni is required before the data can be immutably ledgered. The consensus process can occasionally add a delay of up to several seconds to mutation requests.

If you don't want or need to wait for this process to complete, you can use the syncMode option with a value of ASYNC — this will cause the mutation to be performed asynchronously, and the promise returned from the method will resolve as soon as your node has received the request. You'll be provided a transactionId that can be used to check the status of the mutation later along with the _id of the item you're adding/updating/removing.

const response = await entities.product.add(
  {
    name: 'super-widget',
    inventory: 100,
  },
  {
    syncMode: 'ASYNC',
  }
);

console.log(response?.transaction?.transactionId);
console.log(response?.transaction?._id);

Working with ACLs

Vendia allows you to control read and write access to the data in your Uni via access control lists (ACLs). ACLs can be powerful, but are completely optional — you can read more about ACLs here.

Adding ACLs to mutations

ACLs can be passed to add/update mutations via the aclInput option. Note that this option will only be available for entities that have ACLs enabled in your Uni's schema.

// Add an item with an ACL specifying that all nodes (aside from you) are restricted to read-only access
const response = await entities.product.add(
  {
    name: 'read-only-widget',
    inventory: 100,
  },
  {
    aclInput: {
      acl: [
        {
          principal: {
            nodes: ['*'],
          },
          operations: ['READ'],
        },
      ],
    },
  }
);

Handling aliasing in ACL responses

The client will automatically take care of the complexity of adding and removing aliases when working with ACLs. Feel free to read the detailed explanation below if you're curious.

Detailed explanation

Enabling ACLs adds some complexity to the underlying GraphQL queries required to retrieve data — the queries must be GraphQL unions containing the usual response shape along with a "partial" version (wherein each top-level property of the response becomes optional). This is needed because the data being requested might include fields that the requesting party (you, in this context) does not have permission to read (due to ACLs added by another party sharing the same Uni). GraphQL, unfortunately, does not provide a simple way to express this notion (e.g. I would like data with a name field of type String!, but leave that out of the response if I don't have permission to read it.)

Formatting the requested data as a GraphQL union, in turn, necessitates the use of GraphQL field aliases for one side of the union to prevent type errors. For example, a required name field will have the type String!, but this same name field will have type String! on the "partial" side of the GraphQL union and will therefore need to aliased with something like alias_name: name.

query MyQuery {
  list_Product {
    _Product {
      ... on Self_Product {
        name # <--- GraphQL type of String!
        color # <-- GraphQL type of String!
      }
      ... on Self_Product_Partial_ {
        alias_name: name # <---- GraphQL type of String, alias required!
        alias_color: color # <-- GraphQL type of String, alias required!
      }
    }
  }
}

Fortunately, the client will take care of both adding field aliases to the underlying GraphQL queries of get and list (and the result selector of synchronous add and update) operations and stripping the aliases from responses automatically.

Realtime Data (GraphQL Subscriptions)

The client makes it easy to use GraphQL subscriptions to respond to data updates in realtime. Changes to entities, blocks, files, settings, and more can all be subscribed to using the following format:

const { entities } = client;

entities.product.onAdd((data) => {
  alert(`A new product named ${data.result.name} been added!`);
});

entities.product.onUpdate((data) => {
  alert(`An existing product named ${data.result.name} has been updated!`);
});

entities.product.onRemove((data) => {
  alert(`An existing product named ${data.result.name} has been removed!`);
});

Non-entity types such as blocks and files follow the same format:

const { blocks, files } = client;

blocks.onAdd((data) => {
  alert(`Block ${data.result.blockId} has been minted!`);
});

files.onUpdate((data) => {
  alert(`${data.result.destinationKey} has changed!`);
});

Subscriptions return an unsubscribe method which can be used to terminate the subscription:

const unsubscribe = entities.product.onAdd((data) => console.log(data));

// No longer interested!
unsubscribe();

Storage

File/folder operations are accessed via the files and folders namespaces located under the storage namespace. Learn more about Vendia file storage.

The client currently supports copying files from existing S3 buckets and retrieving metadata about files on your Uni.

Coming soon: support for directly uploading and retrieving files to your Uni from the client!

const { storage } = client;
const documentsFolder = 'documents';

const addFolderResponse = await storage.folders.add({
  name: documentsFolder,
});

const addFileResponse = await storage.files.add({
  destinationKey: `${documentsFolder}/my-document.txt`,
  sourceKey: 'my-document.txt',
  sourceBucket: 'my-bucket',
  sourceRegion: 'us-east-1',
});

const getFileResponse = await storage.files.get(addFileResponse._id);
console.log(`My document is available at ${getFileResponse.destinationKey}!`);

Blocks

The entire history of your Uni is available via the blocks namespace. Blocks can be accessed via get or list operations, and the onAdd subscription can be used to react to newly minted blocks in realtime. Learn more about "blocks" and other Vendia terminology here.

const { blocks } = client;

const getResponse = await blocks.get('example-block-id-abc-123');
const listResponse = await client.blocks.list();

Settings

Various settings can be queried and updated using the settings namespace (e.g., auth, success/error notifications). Use the get and update operations to retrieve and update settings. The onUpdate subscription can be used to react to settings changes in realtime.

const response = await client.settings.get();

Uni Info

The uniInfo namespace provides access to information about your Uni (e.g., its name, schema, and info about each participating node in the Uni). Use the get operation (no arguments required) to retrieve the info and the onUpdate subscription to react to changes in realtime.

const response = await client.uniInfo.get();

Smart Contracts

You can use the smartContracts namespace to interact with your Uni's smart contracts. Use the add, get, list, update, and remove operations to perform CRUD operations on smart contracts.

const { contracts } = client;

const response = await contracts.add({
  name: 'update-delivery-status',
  resource: {
    uri: 'arn:aws:lambda:us-west-2:123456789012:function:ContractEnforcement:9',
  },
  description:
    'a smart contract that updates the delivery status of a shipment',
  inputQuery:
    'query shipmentDetails($id: ID!) { getShipment(id: $id) { _id orderId destinationWarehouse }}',
  outputMutation:
    'mutation m($id: ID!, $delivered: Boolean, $lastUpdated: String, $orderId: String) { updateShipment(id: $id, input: { delivered: $delivered, lastUpdated: $lastUpdated, orderId: $orderId }, syncMode: ASYNC) { transaction { _id } } }',
});

Smart contracts can be invoked with the invoke method.

const response = await contracts.invoke('example-contract-id-abc-123');

You can also use the onAdd, onUpdate, and onRemove subscriptions to react to changes in realtime. Read all about Smart Contracts here.

Custom GraphQL requests

It is possible to make custom GraphQL requests via the request method. This shouldn't be necessary often, but there are a few scenarios that the client does not directly support where custom requests might prove useful:

  1. Requesting a subset of fields — the client always requests every field of a given data type which may be more data than you need.
  2. Executing multiple queries or mutations in a single request (also known as "batching").
  3. Executing Vendia Transactions, which require batching mutations in a single request, and additionally guarantee that mutations will be performed serially, in order, as an atomic unit.

Requesting a subset of fields

// Requesting a subset of fields on the Vendia Block type
const listBlocksQuery = `
  query listBlocks {
    listVendia_BlockItems {
      Vendia_BlockItems {
        blockId
      }
      nextToken
    }
  }
`;
const response = await client.request(listBlocksQuery);

// Returns the full GraphQL "data" response
console.log(response?.listVendia_BlockItems?.Vendia_BlockItems?.[0]?.blockId);

Executing a Vendia Transaction

const vendiaTransactionQuery = `
mutation exampleMutation @vendia_transaction { # <-- vendia_transaction directive
  update_Product(
    id: "abc-123"
    input: { inventory: 99 }
    syncMode: NODE_LEDGERED
  ) {
    result {
      inventory
    }
  }
  update_User(
    id: "def-456"
    input: { products_purchased : 2 }
    syncMode: NODE_LEDGERED
  ) {
    result {
      products_purchased
    }
    transaction {
      transactionId
    }
  }
}
`;
const response = await client.request(vendiaTransactionQuery);
console.log(response?.update_User?.transaction?.transactionId);

Fixes for common issues

Issue: Received Error: Cannot find module '../../.vendia-client/index' when trying to build or run my code.

This error means that code generation did not complete successfully. The most likely cause is that the .vendia folder is missing from the root of your project. Please follow the instruction above for pulling your Uni's schema.

Issue: I received an error during installation or code generation.

Potential causes include the following:

  1. The .vendia folder is missing from the root of your project.
  2. If you've updated @vendia/client to a new version which takes advantage of changes/additions to the core Vendia platform, you may need to pull the latest version of your Uni's schema. In this case, it's the generated GraphQL schema rather than your JSON schema that may be out of date. This should only happen when bumping major versions of the client, but we're still in alpha at the moment and moving very quickly!

In either case, please follow the instruction above for pulling the latest version of your Uni's schema and follow the CLI prompts to execute code generation afterwards.

Issue: client.entities is an empty object! It doesn't contain any of the entities described by my Uni's JSON schema.

Again, the most likely cause is that the .vendia folder is missing from the root of your project.

If the post-installation script can not locate the .vendia folder, it will fall back to using a generic Vendia schema that doesn't contain any of the entities described by your Uni's JSON schema — this results in an empty client.entities namespace (though all other aspects of client functionality should work). Please follow the instruction above for pulling your Uni's schema.

Issue: code completion (intellisense) doesn't work in WebStorm IDE (JavaScript projects only - should always work in TypeScript projects).

Workaround: Open preferenceslanguages and frameworksjavascriptlibrariesadd+ button → use command + shift + . to show hidden folders (on Mac, not sure about Windows - apologies), select .vendia-client directory. This adds all the types to the project.

Appendix

Schema evolution

Vendia allows you to evolve your schema as your data sharing requirements change — you can read more about schema evolution here. Whenever you evolve your schema, you'll need to update the schema files stored in your .vendia directory and generate new client code.

Use the Vendia CLI to run

share client:pull

This will pull the latest schema files to your .vendia directory and then issue the following prompt:

Would you like to update the auto-generated client code based on the latest schema?
This is highly recommended. (Y/n)

Tap Enter to continue and you're done!

Code generation details

The @vendia/client package consists of a lightweight wrapper (the exported createVendiaClient function) along with a suite of tools used to dynamically generate TypeScript files based on your schema. When the package is installed/updated via npm/yarn/pnpm, a post-installation script will attempt to perform the following steps:

  1. Find the .vendia directory in the root of your project and read the schema/config files inside.
  2. Use the schema/config data to generate TypeScript code tailored to your Uni's schema.
  3. Create a .vendia-client directory inside node_modules
  4. Copy the wrapper TypeScript source code (createVendiaClient) and into node_modules/.vendia-client.
  5. Write the generated TypeScript code to node_modules/.vendia-client/generated.ts.
  6. Compile the TypeScript code to JavaScript and type declaration (*.d.ts) files.

If there is no .vendia directory, a generic Vendia schema that doesn't contain any "entities" (the data types defined by your Uni's JSON schema) will be used, resulting in an empty client.entities namespace (though all other aspects of client functionality should work).

This project was heavily inspired by Prisma's awesome type-safe client and made possible by a suite of amazing open-source tools; especially the incredible GraphQL Code Generator!

FAQs

Why is the .vendia-client directory created in node_modules?

There are a few reasons for this:

  1. This allows for a simpler installation process with very little configuration.
  2. It allows the client to be imported from a consistent location (e.g., import { createVendiaClient } from '@vendia/client').
  3. We would prefer not to surprise anyone by modifying files outside of node_modules during a package installation.

Can I specify an alternate path for the generated files?

Not at this time. Please let us know if you need this!

Show table of contents
Edit this page