How to create a new TypeDB Driver
This tutorial can guide us through the very beginning of creating a new TypeDB Driver. We strongly recommend using one of the existing TypeDB Drivers first to gain some experience with the TypeDB. See the list of available Drivers on the Clients overview page.
TypeDB Driver is an important part of any TypeDB Client. We will use both terms in this tutorial, and it’s important not to be confused by what is what. TypeDB Client — any software that can connect to TypeDB and provide us with some kind of interface we can use: API, CLI, or GUI. TypeDB Driver — a library that implements TypeDB Client RPC protocol to connect to TypeDB and provides an API to use it. Usually, Drivers are used as a part of other applications to connect to TypeDB. For convenience, the term TypeDB Clients includes all TypeDB Drivers. |
There are many places we could start building a TypeDB Driver. In this tutorial, we start by attempting to make a single gRPC call to a TypeDB server to create a database.
Step 1: Create the main function to connect to a server
Create a TypeDB
source file in the root of the project, which should expose a function named coreClient
,
taking address
as a parameter.
Import statements are not included in this tutorial, except when importing from external libraries such as the TypeDB protobuf definitions. |
-
Java
-
Python
-
Node.js
// TypeDB.java
public class TypeDB {
public static TypeDBClient coreClient(String address) {
return new CoreClient(address);
}
}
# typedb/client.py (named to allow importing from typedb.client)
class TypeDB:
@staticmethod
def core_client(address: str, parallelisation: int = 2) -> TypeDBClient:
return _CoreClient(address, parallelisation)
// TypeDB.ts
export namespace TypeDB {
export function coreClient(address: string): TypeDBClient {
return new CoreClient(address);
}
}
Step 2: Database manager
TypeDBClient
is not yet defined. Create a new directory named api/connection
and create a TypeDBClient
file there:
If the selected language doesn’t have interfaces or abstract classes, make |
-
Java
-
Python
-
Node.js
// api/connection/TypeDBClient.java
public interface TypeDBClient extends AutoCloseable {
DatabaseManager databases();
void close();
}
# typedb/api/connection/client.py
from abc import ABC, abstractmethod
class TypeDBClient(ABC):
@abstractmethod
def databases(self) -> DatabaseManager:
pass
@abstractmethod
def close(self) -> None:
pass
@abstractmethod
def __enter__(self):
pass
@abstractmethod
def __exit__(self, exc_type, exc_val, exc_tb):
pass
// api/connection/TypeDBClient.ts
export interface TypeDBClient {
readonly databases: DatabaseManager;
close(): Promise<void>;
}
Step 3: gRPC connection implementation
The next step is to implement connection/TypeDBClient
and its subclass connection/core/CoreClient
.
Create the directory structure: connection/core
in the root of the project.
Name the classes depending on language conventions: in Java/TypeScript, TypeDBClientImpl
and CoreClient
; in Python,
_TypeDBClient
and _CoreClient
.
Ensure that gRPC is imported into the project, and refer to the gRPC docs to learn how to create a Channel — the code varies by language.
In languages with no inheritance, adhere to this project structure as closely as possible, perhaps by writing top-level functions in the respective locations. |
-
Java
-
Python
-
Node.js
// connection/TypeDBClientImpl.java
public abstract class TypeDBClientImpl implements TypeDBClient {
private final TypeDBDatabaseManagerImpl databaseMgr;
protected TypeDBClientImpl() {
databaseMgr = new TypeDBDatabaseManagerImpl(this);
}
@Override
public TypeDBDatabaseManagerImpl databases() {
return databaseMgr;
}
public abstract ManagedChannel channel();
public abstract TypeDBStub stub();
@Override
public void close() {
try {
channel().shutdown().awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// connection/core/CoreClient.java
public class CoreClient extends TypeDBClientImpl {
private final ManagedChannel channel;
private final TypeDBStub stub;
public CoreClient(String address) {
super();
channel = NettyChannelBuilder.forTarget(address).usePlaintext().build();
stub = CoreStub.create(channel);
}
@Override
public ManagedChannel channel() {
return channel;
}
@Override
public TypeDBStub stub() {
return stub;
}
}
# typedb/connection/client.py
class _TypeDBClientImpl(TypeDBClient):
def __init__(self):
pass
def databases(self) -> _TypeDBDatabaseManagerImpl:
pass
def stub(self) -> TypeDBStub:
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
if exc_tb is not None:
return False
def close(self) -> None:
pass
# typedb/connection/core/client.py
from grpc import Channel, insecure_channel
class _CoreClient(_TypeDBClientImpl):
def __init__(self, address: str):
super(_CoreClient, self).__init__()
self._channel = insecure_channel(address)
self._stub = _CoreStub(self._channel)
self._databases = _TypeDBDatabaseManagerImpl(self._stub)
def databases(self) -> _TypeDBDatabaseManagerImpl:
return self._databases
def stub(self) -> _CoreStub:
return self._stub
def close(self) -> None:
super().close()
self._channel.close()
// connection/TypeDBClientImpl.ts
export abstract class TypeDBClientImpl implements TypeDBClient {
private _isOpen: boolean;
protected constructor() {
this._isOpen = true;
}
isOpen(): boolean {
return this._isOpen;
}
abstract get databases(): TypeDBDatabaseManagerImpl;
abstract stub(): TypeDBStub;
async close(): Promise<void> {
if (this._isOpen) {
this._isOpen = false;
}
}
}
// connection/core/CoreClient.ts
export class CoreClient extends TypeDBClientImpl {
private readonly _stub: CoreStub;
private readonly _databases: TypeDBDatabaseManagerImpl;
constructor(address: string) {
super();
this._stub = new CoreStub(address);
this._databases = new TypeDBDatabaseManagerImpl(this._stub);
}
get databases(): TypeDBDatabaseManagerImpl {
return this._databases;
}
stub(): TypeDBStub {
return this._stub;
}
async close(): Promise<void> {
await super.close();
this._stub.close();
}
}
Step 4: Implementing database creation
Finally, we implement DatabaseManager
, and CoreStub
to set up gRPC calls to the server.
We’ll need to compile TypeDB’s protocol in order to do this. Most languages have protobuf compilers that we can use to generate a TypeDB protocol library for the language we use. |
-
Java
-
Python
-
Node.js
// api/database/DatabaseManager.java
public interface DatabaseManager {
void create(String name);
}
// connection/TypeDBDatabaseManagerImpl.java
import com.vaticle.typedb.protocol.CoreDatabaseProto;
public class TypeDBDatabaseManagerImpl implements DatabaseManager {
private final TypeDBClientImpl client;
public TypeDBDatabaseManagerImpl(TypeDBClientImpl client) {
this.client = client;
}
@Override
public void create(String name) {
stub().databasesCreate(CoreDatabaseProto.CoreDatabaseManager.Create.Req.newBuilder().setName(name).build());
}
TypeDBStub stub() {
return client.stub();
}
}
// common/rpc/TypeDBStub.java
import com.vaticle.typedb.protocol.CoreDatabaseProto;
import com.vaticle.typedb.protocol.TypeDBGrpc;
public abstract class TypeDBStub {
public CoreDatabaseProto.CoreDatabaseManager.Create.Res databasesCreate(CoreDatabaseProto.CoreDatabaseManager.Create.Req request) {
return blockingStub().databasesCreate(request);
}
protected abstract TypeDBGrpc.TypeDBBlockingStub blockingStub();
}
// connection/core/CoreStub.java
import com.vaticle.typedb.protocol.TypeDBGrpc;
import io.grpc.ManagedChannel;
public class CoreStub extends TypeDBStub {
private final ManagedChannel channel;
private final TypeDBGrpc.TypeDBBlockingStub blockingStub;
private CoreStub(ManagedChannel channel) {
super();
this.channel = channel;
this.blockingStub = TypeDBGrpc.newBlockingStub(channel);
}
public static CoreStub create(ManagedChannel channel) {
return new CoreStub(channel);
}
@Override
protected TypeDBGrpc.TypeDBBlockingStub blockingStub() {
return blockingStub;
}
}
# typedb/api/connection/database.py
from abc import ABC, abstractmethod
class DatabaseManager(ABC):
@abstractmethod
def create(self, name: str) -> None:
pass
# typedb/connection/database_manager.py
import typedb_protocol.core.core_database_pb2 as core_database_proto
class _TypeDBDatabaseManagerImpl(DatabaseManager):
def __init__(self, stub: TypeDBStub):
self._stub = stub
def create(self, name: str) -> None:
req = core_database_proto.CoreDatabaseManager.Create.Req()
req.name = name
self._stub.databases_create(req)
def stub(self) -> TypeDBStub:
return self._stub
# typedb/common/rpc/stub.py
import typedb_protocol.core.core_database_pb2 as core_database_proto
import typedb_protocol.core.core_service_pb2_grpc as core_service_proto
class TypeDBStub(ABC):
def databases_create(self, req: core_database_proto.CoreDatabaseManager.Create.Req) -> core_database_proto.CoreDatabaseManager.Create.Res:
return self.stub().databases_create(req)
def stub(self) -> core_service_proto.TypeDBStub:
pass
# typedb/connection/core/stub.py
from grpc import Channel
import typedb_protocol.core.core_service_pb2_grpc as core_service_proto
class _CoreStub(TypeDBStub):
def __init__(self, channel: Channel):
super(_CoreStub, self).__init__()
self._channel = channel
self._stub = core_service_proto.TypeDBStub(channel)
def stub(self) -> TypeDBStub:
return self._stub
// api/connection/database/TypeDBClient.ts
export interface DatabaseManager {
create(name: string): Promise<void>;
}
// connection/TypeDBDatabaseManagerImpl.ts
import { CoreDatabaseManager } from "typedb-protocol/core/core_database_pb";
export class TypeDBDatabaseManagerImpl implements DatabaseManager {
private readonly _stub: TypeDBStub;
constructor(client: TypeDBStub) {
this._stub = client;
}
public create(name: string): Promise<void> {
return this._stub.databasesCreate(new CoreDatabaseManager.Create.Req().setName(name));
}
stub() {
return this._stub;
}
}
// common/rpc/TypeDBStub.ts
import { CoreDatabaseManager } from "typedb-protocol/core/core_database_pb";
import { TypeDBClient } from "typedb-protocol/core/core_service_grpc_pb";
export abstract class TypeDBStub {
databasesCreate(req: CoreDatabaseManager.Create.Req): Promise<void> {
return new Promise((resolve, reject) => {
this.stub().databases_create(req, (err) => {
if (err) reject(new Error(err));
else resolve();
})
});
}
abstract stub(): TypeDBClient;
}
// connection/core/CoreStub.ts
import { ChannelCredentials } from "@grpc/grpc-js";
import { TypeDBClient } from "typedb-protocol/core/core_service_grpc_pb";
export class CoreStub extends TypeDBStub {
private readonly _stub: TypeDBClient;
constructor(address: string) {
super();
this._stub = new TypeDBClient(address, ChannelCredentials.createInsecure());
}
stub(): TypeDBClient {
return this._stub;
}
close(): void {
this._stub.close();
}
}
Step 5: Testing
At this point, we have all the necessary components to create a database! Run the TypeDB server locally and create a test function:
-
Java
-
Python
-
Node.js
public static void typeDBClientTest() {
try (TypeDBClient client = TypeDB.coreClient("127.0.0.1:1729")) {
client.databases().create("typedb");
}
}
def typedb_client_test():
with TypeDB.core_client("127.0.0.1:1729") as client:
client.databases().create("typedb")
async function typeDBClientTest() {
try {
const client = TypeDB.coreClient("127.0.0.1:1729");
await client.databases().create("typedb");
} finally {
client?.close();
}
}
Run the test function.
Now we can verify that the database was created successfully using
TypeDB Console database list
command, or
by running the test again (which will throw an error saying that the database already exists).
That concludes the basics tutorial for creating a new TypeDB Driver.
Refer to the Developing a new TypeDB Driver page for more information of the remaining components needed to open transactions, run queries, and take the Driver to 100% completion.
We recommend using one of our existing Drivers as a reference, and copying the implementation into the chosen language.