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
- Navigate to the root directory of an existing project you’ve been working on or create a new, empty directory and
cd
into it. - 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, requiredwebsocketUrl
- string, optional (but required in order to use GraphQL subscriptions)apiKey
- string, optionaldebug
- boolean, optional (set totrue
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'],
},
],
},
}
)
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:
- Requesting a subset of fields — the client always requests every field of a given data type which may be more data than you need.
- Executing multiple queries or mutations in a single request (also known as "batching").
- 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:
- The
.vendia
folder is missing from the root of your project. - 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: I'm using pnpm
to install dependencies and getting code-gen or typescript compilation errors
pnpm
uses symlinks in node_modules to enable some dependency management optimizations. Unfortunately, these symlinks can confuse TypeScript's compiler (tsc
). If you're using pnpm
and running into issues, you can either try using npm
instead, or you can add an .npmrc
file to the root of your project with the following line:
node-linker=hoisted
This disables some of pnpm
's optimizations, but should resolve the issue.
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 preferences
→ languages and frameworks
→ javascript
→ libraries
→ add
→ + 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:
- Find the
.vendia
directory in the root of your project and read the schema/config files inside. - Use the schema/config data to generate TypeScript code tailored to your Uni's schema.
- Create a
.vendia-client
directory insidenode_modules
- Copy the wrapper TypeScript source code (
createVendiaClient
) and intonode_modules/.vendia-client
. - Write the generated TypeScript code to
node_modules/.vendia-client/generated.ts
. - 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:
- This allows for a simpler installation process with very little configuration.
- It allows the client to be imported from a consistent location (e.g.,
import { createVendiaClient } from '@vendia/client'
). - 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!