Skip to content

Commit

Permalink
Build your own connector. Temp home in docs while Academy is paused. (#…
Browse files Browse the repository at this point in the history
…411)

Co-authored-by: Rob Dominguez <[email protected]>
  • Loading branch information
seanparkross and robertjdominguez committed Jun 28, 2024
1 parent 6bb3ae2 commit 65401fe
Show file tree
Hide file tree
Showing 31 changed files with 2,339 additions and 14 deletions.
38 changes: 38 additions & 0 deletions docs/connectors/build-your-own/00-get-started/01-clone.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: "Clone the Repo"
metaTitle: 'Clone the Repo | Hasura DDN Data Connector Tutorial'
metaDescription: 'Learn how to build a data connector in Typescript for Hasura DDN'
---

Clone the [repo](https://github.com/hasura/ndc-typescript-learn-course) for this project to get started.

```shell
# If using GitHub with SSH
git clone [email protected]:hasura/ndc-typescript-learn-course.git
# OR if using GitHub with HTTPS
git clone https://github.com/hasura/ndc-typescript-learn-course.git
```

You'll see in the main branch two files: `finishedIndex.ts` and `index.ts`. The `finishedIndex.ts` file contains the
completed implementation of the connector as reference, while the `index.ts` file is empty allowing you to follow along.

Then install the dependencies:
```shell
npm install
```

You can build and run the connector, when you need to, with:
```shell
npm run build && node dist/index.js serve --configuration .
```

However, you can run nodemon to watch for changes and rebuild automatically:

```shell
npm run dev
```

[//]: # (TODO: Cannot find more information about the configuration file creation and usage.)

_Note: the configuration.json file is a pre-configured file which gives the connector information about the data
source._
75 changes: 75 additions & 0 deletions docs/connectors/build-your-own/00-get-started/02-basics.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: "Basic Setup"
metaTitle: 'Basic Setup | Hasura DDN Data Connector Tutorial'
metaDescription: 'Learn how to build a data connector in Typescript for Hasura DDN'
---

Let's set up the scaffolding for our connector, and we'll see the first queries start to work. We'll also start to
develop a test suite, and see our connector running in Hasura.

For now, we'll just handle the most basic queries, but later, we'll start to fill in some of the gaps in our
implementation, and see more queries return results correctly.

[//]: # (TODO: This: "We'll also cover topics such as metrics, connector configuration, error reporting, and tracing.
" is not implemented - possibly Phil Freeman will do this in the future?)

The data source you'll be targeting is a SQLite database running on your local machine, and we'll be using the Hasura
[TypeScript connector SDK](https://github.com/hasura/ndc-sdk-typescript).

If you've cloned the repo in the previous step, you can follow along with the code in this tutorial.

Let's start by following the [SDK guidelines](https://github.com/hasura/ndc-sdk-typescript) and use the `start`
function which take a `connector` of type `Connector`.

## Start

In your `src/index.ts` file, add the following:

```typescript
const connector: Connector<Configuration, State> = {};

start(connector);
```

We will also need some imports over the course of the tutorial. Paste these at the top of your index.ts file:

```typescript
import opentelemetry from "@opentelemetry/api";
import sqlite3 from "sqlite3";
import { readFile } from "fs/promises";
import { resolve } from "path";
import { Database, open } from "sqlite";
import {
BadGateway,
BadRequest,
CapabilitiesResponse,
CollectionInfo,
ComparisonTarget,
ComparisonValue,
Connector,
ConnectorError,
ExplainResponse,
Expression,
ForeignKeyConstraint,
InternalServerError,
MutationRequest,
MutationResponse,
NotSupported,
ObjectField,
ObjectType,
OrderByElement,
Query,
QueryRequest,
QueryResponse,
Relationship,
RowFieldValue,
ScalarType,
SchemaResponse,
start,
} from "@hasura/ndc-sdk-typescript";
import { withActiveSpan } from "@hasura/ndc-sdk-typescript/instrumentation";
import { Counter, Registry } from "prom-client";
```

You'll notice that your IDE will complain about the `connector` object not having the correct type, and
`Configuration, State` all being undefined. Let's fix that in the next section...
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: "Configuration and State"
metaTitle: 'Configuration and State | Hasura DDN Data Connector Tutorial'
metaDescription: 'Learn how to build a data connector in Typescript for Hasura DDN'
---

We need to fill in implementations for each of the required functions, but we won't need all of these to work just yet.

First, you'll see that we define two types: `Configuration`, and `State`.

Let's define those now above the `connector` and `start` function:

```typescript
type Configuration = {
tables: TableConfiguration[];
filename: string;
};

type TableConfiguration = {
tableName: string;
columns: { [k: string]: Column };
};

type Column = {};

type State = {
db: Database;
};
```

`Configuration` is the type of the connector's configuration, which will be read from a directory on disk. By
convention, this configuration should be enough to reproducibly determine the NDC schema, so for our SQLite connector,
we configure the connector with a list of tables that we want to expose. Each table is defined by its name and a list of
columns. Columns don't have any specific configuration yet, but we leave an empty object type here because we might want
to capture things like column types later on.

[//]: # (TODO: What does it mean to validate the configuration? What does it mean to have a validated configuration?)

## State

The `State` type is for things like connection pools, handles, or any non-serializable state that gets allocated on
startup, and which lives for the lifetime of the connector. For our connector, we need to keep a handle to our sqlite
database.

Cool, so now that we've got our types defined, we can fill in the function definitions which the connector requires
in order to interact with our SQLite database and Hasura DDN. Let's do that in the next step.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
title: "Function Definitions"
metaTitle: 'Function Definitions | Hasura DDN Data Connector Tutorial'
metaDescription: 'Learn how to build a data connector in Typescript for Hasura DDN'
---

Now let's fill in some function definitions, these are the functions required to provide to the connector to satisfy
the Hasura connector specification, and we'll be implementing them as we go through the course.

Copy and paste the following required functions into the `src/index.ts` file. Note that the amended `connector` is
also included at the bottom, overwriting the previous connector definition with this which takes these functions as
arguments.

```typescript
async function parseConfiguration(configurationDir: string): Promise<Configuration> {
throw new Error("Function not implemented.");
}

async function tryInitState(configuration: Configuration, registry: Registry): Promise<State> {
throw new Error("Function not implemented.");
}

function getCapabilities(configuration: Configuration): CapabilitiesResponse {
throw new Error("Function not implemented.");
}

async function getSchema(configuration: Configuration): Promise<SchemaResponse> {
throw new Error("Function not implemented.");
}

async function query(configuration: Configuration, state: State, request: QueryRequest): Promise<QueryResponse> {
throw new Error("Function not implemented.");
}

async function fetchMetrics(configuration: Configuration, state: State): Promise<undefined> {
throw new Error("Function not implemented.");
}

async function healthCheck(configuration: Configuration, state: State): Promise<undefined> {
throw new Error("Function not implemented.");
}

async function queryExplain(configuration: Configuration, state: State, request: QueryRequest): Promise<ExplainResponse> {
throw new Error("Function not implemented.");
}

async function mutationExplain(configuration: Configuration, state: State, request: MutationRequest): Promise<ExplainResponse> {
throw new Error("Function not implemented.");
}

async function mutation(configuration: Configuration, state: State, request: MutationRequest): Promise<MutationResponse> {
throw new Error("Function not implemented.");
}
```

Now we need to update the `connector` definition to include these functions.

```typescript
const connector: Connector<Configuration, State> = {
parseConfiguration,
tryInitState,
getCapabilities,
getSchema,
query,
fetchMetrics,
healthCheck,
queryExplain,
mutationExplain,
mutation
};
```

Ok, moving on swiftly, for this course we will only need to implement the first five functions:
- `parseConfiguration`: which reads the configuration from files on disk.
- `tryInitState`: which initializes our database connection.
- `getCapabilities`: which returns the NDC capabilities of our connector.
- `getSchema`: which returns an NDC schema containing our tables and columns.
- `query`: which actually responds to query requests.

Let's do that now.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: "Implementation"
metaTitle: 'Implementation | Hasura DDN Data Connector Tutorial'
metaDescription: 'Learn how to build a data connector in Typescript for Hasura DDN'
---

Right now, we only need to implement five required functions:
- `parseConfiguration`: which reads the configuration from files on disk.
- `tryInitState`: which initializes our database connection.
- `getCapabilities`: which returns the NDC capabilities of our connector.
- `getSchema`: which returns an NDC schema containing our tables and columns.
- `query`: which actually responds to query requests.

We'll skip configuration validation entirely for now, and just read the raw configuration from a `configuration.json`
file in the configuration directory:

```typescript
async function parseConfiguration(configurationDir: string): Promise<Configuration> {
const configuration_file = resolve(configurationDir, 'configuration.json');
const configuration_data = await readFile(configuration_file);
const configuration = JSON.parse(configuration_data.toString());
return {
filename: resolve(configurationDir, 'database.db'),
...configuration
};
}
```

To initialize our state, which in our case contains a connection to the database, we'll use the `open` function to
open a connection to it, and store the resulting connection object in our state by returning it:

```typescript
async function tryInitState(
configuration: Configuration,
registry: Registry
): Promise<State> {
const db = await open({
filename: configuration.filename,
driver: sqlite3.Database
});

return { db };
}
```

[//]: # (TODO: Link to the relevant part of the spec)
Our capabilities response will be very simple, because we won't support many capabilities yet. We just return the
version range of the specification that we are compatible with, and the basic `query` and `mutation` capabilities.

```typescript
function getCapabilities(configuration: Configuration): CapabilitiesResponse {
return {
version: "0.1.2",
capabilities: {
query: {},
mutation: {}
}
}
}
```
Loading

0 comments on commit 65401fe

Please sign in to comment.