This sample application uses the RDG web stack. It shows how you can set up a GraphQL server using a Dolt database and connect it to a React front end.
~ % yarn && yarn compile
In order to start your GraphQL server, you need to provide your database configuration. Add a .development.env
file that looks like this:
HOST="dolthub-us-jails.dbs.hosted.doltdb.com"
PORT=3306
USERNAME="myusername"
PASSWORD="mypassword"
DATABASE="us_jails"
Note: we are using a cloud-hosted Dolt database from Hosted Dolt
You'll know if your database is configured correctly if you start your local GraphQL server and get no errors.
In packages/graphql-server
:
graphql-server % yarn dev
[3:00:17 PM] Starting compilation in watch mode...
[3:00:21 PM] Found 0 errors. Watching for file changes.
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [NestFactory] Starting Nest application...
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] TypeOrmModule dependencies initialized +47ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] ConfigHostModule dependencies initialized +1ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] TerminusModule dependencies initialized +0ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] GraphQLSchemaBuilderModule dependencies initialized +92ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] GraphQLModule dependencies initialized +3ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +564ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] TypeOrmModule dependencies initialized +1ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] DoltBranchesModule dependencies initialized +5ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [RoutesResolver] DoltBranchesController {/doltBranches}: +24ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [RouterExplorer] Mapped {/doltBranches, GET} route +4ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [GraphQLModule] Mapped {/graphql, POST} route +146ms
[Nest] 5310 - 07/31/2023, 3:00:24 PM LOG [NestApplication] Nest application successfully started +5ms
You can test your queries in the GraphQL playground from http://localhost:9000/graphql.
In another terminal, start the local web server. Go to packages/web
and run:
web % yarn dev
- ready started server on 0.0.0.0:3000, url: http://localhost:3000
- info Loaded env from /Users/me/dern-sample-app/packages/web/.env.development
- event compiled client and server successfully in 1816 ms (18 modules)
And then navigate to http://localhost:3000 in your browser.
Dolt is a MySQL-compatible version-controlled database, which includes things like branches, commits, diffs, and merges. Dolt can be used with any MySQL client, including Node MySQL, which is used in this application.
Dolt has a cloud-hosted option called Hosted Dolt, which is great for creating production applications.
The GraphQL server layer sits between Dolt and the React web application. We use the Nest.js framework to integrate with a Dolt database via TypeORM, as well as expose GraphQL endpoints via the GraphQL integration.
This application uses Next.js (a React framework that abstracts and automatically configures tooling needed for React, like bundling, compiling), Apollo Client (manages local and remote data with GraphQL), and GraphQL Code Generator (generates Typescript types and React query and mutation hooks based on our GraphQL schema).
We use the NestJS framework to build an efficient, scalable Node.js server-side application. It provides a level of abstraction above common Node.js frameworks like Express, which makes it easy to support both Typescript and GraphQL and to integrate with any database.
We use two built-in Nest integrations to build our GraphQL server:
- TypeORM, the most mature ORM available for Typescript. We use the MySQL database driver with TypeORM to connect to our Dolt SQL server.
- GraphQL, a powerful query language for APIs and a runtime for fulfilling those queries with your existing data. We can configure the GraphQL module to use Apollo server, a service that processes GraphQL operations from application clients. We'll come back to Apollo when we set up the React portion of our application.
You can follow these steps to set up a new project.
We use the TypeORM
module to populate
connectivity information of our Dolt database into the root AppModule
. You can find this
information in the Connectivity tab of your Hosted Dolt deployment.
// src/app.module.ts
import { Module } from "@nestjs/common";
import { TerminusModule } from "@nestjs/terminus";
import { TypeOrmModule } from "@nestjs/typeorm";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "mysql",
host: "dolthub-us-jails.dbs.hosted.doltdb.com",
port: 3306,
username: "myusername",
password: "xxxxxx",
database: "us_jails",
ssl: {
rejectUnauthorized: false,
},
autoLoadEntities: true,
synchronize: false,
}),
TerminusModule,
],
})
export class AppModule {}
The example in this repository uses the config module here instead to get your database information from an env file.
TypeORM supports the repository design pattern, so each entity will have its own
repository. Entity
is a class that maps to a database table. We will use the
dolt_branches
system table in this example.
mysql> select * from dolt_branches;
+-------------+----------------------------------+---------------------+------------------------+-------------------------+-------------------------+
| name | hash | latest_committer | latest_committer_email | latest_commit_date | latest_commit_message |
+-------------+----------------------------------+---------------------+------------------------+-------------------------+-------------------------+
| delete-rows | u8s83gapv7ghnbmrtpm8q5es0dbl7lpd | taylorb | taylor@dolthub.com | 2022-06-14 19:11:58.402 | Accept PR 44 |
| new-branch | sqjm4s0f2m48rjc97hr6cbpv2hqga00d | Dolt System Account | doltuser@dolthub.com | 2022-09-14 19:30:41.132 | delete row |
+-------------+----------------------------------+---------------------+------------------------+-------------------------+-------------------------+
2 rows in set (0.06 sec)
The DoltBranch
entity that maps to the dolt_branches
system table schema.
// src/doltBranches/doltBranch.entity.ts
import { Column, Entity, PrimaryColumn } from "typeorm";
@Entity()
export class DoltBranches {
@PrimaryColumn()
name: string;
@Column()
hash: string;
@Column()
latest_committer: string;
@Column()
latest_committer_email: string;
@Column()
latest_commit_message: string;
@Column()
latest_commit_date: Date;
}
Since we set autoLoadEntities
to true
in our TypeORM module, we can begin using this
entity automatically.
Then we create a DoltBranchesModule
and add it to the imports
in AppModule
.
// src/doltBranches/doltBranch.module.ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { DoltBranches } from "./doltBranch.entity";
import { DoltBranchesService } from "./doltBranch.service";
@Module({
imports: [TypeOrmModule.forFeature([DoltBranches])],
providers: [DoltBranchesService],
exports: [DoltBranchesService],
})
export class DoltBranchesModule {}
The forFeature
method injects the DoltBranchesRepository
into the
DoltBranchesService
using the @InjectRepository()
decorator. This lets us use
different methods to query or mutate
data in that database table.
// src/doltBranches/doltBranch.service.ts
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { DoltBranches } from "./doltBranch.entity";
export class CreateBranchArgs {
newBranchName: string;
refName: string;
}
@Injectable()
export class DoltBranchesService {
constructor(
@InjectRepository(DoltBranches)
private doltBranchesRepository: Repository<DoltBranches>
) {}
findAll(): Promise<DoltBranches[]> {
return this.doltBranchesRepository.find();
}
}
Since we're using the code first approach, we use decorators and Typescript classes to generate the corresponding GraphQL schema.
The GraphQLModule
is imported in AppModule
.
// src/app.module.ts
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { Module } from "@nestjs/common";
import { GraphQLModule } from "@nestjs/graphql";
import { TerminusModule } from "@nestjs/terminus";
import { TypeOrmModule } from "@nestjs/typeorm";
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
autoSchemaFile: "schema.gql",
context: (ctx) => ctx,
driver: ApolloDriver,
}),
TypeOrmModule.forRoot({
// ...database config
}),
TerminusModule,
],
})
export class AppModule {}
Each object type you
define should represent a domain object that an application client might need to interact
with. Our Branch
model looks like this:
// src/branches/branch.model.ts
import { Field, GraphQLTimestamp, ID, ObjectType } from "@nestjs/graphql";
@ObjectType()
export class Branch {
@Field((_type) => ID)
name: string;
@Field()
hash: string;
@Field()
latestCommitter: string;
@Field()
latestCommitterEmail: string;
@Field()
latestCommitMessage: string;
@Field((_type) => GraphQLTimestamp)
latestCommitDate: Date;
}
A Resolver
class is a way for our client to interact with the Branch
object.
// src/branches/branch.resolver.ts
import { Query, Resolver } from "@nestjs/graphql";
import { DoltBranchesService } from "../doltBranches/doltBranch.service";
import { Branch, fromDoltBranchesRow } from "./branch.model";
@Resolver((_of) => Branch)
export class BranchResolver {
constructor(private doltBranchService: DoltBranchesService) {}
@Query((_returns) => [Branch])
async branches(): Promise<Branch[]> {
const branches = await this.doltBranchService.findAll();
return branches.map(fromDoltBranchesRow);
}
}
This resolver needs to be added to AppModule
as a provider. The full AppModule
looks like this:
// src/app.module.ts
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
autoSchemaFile: "schema.gql",
context: (ctx) => ctx,
driver: ApolloDriver,
}),
TypeOrmModule.forRoot({
// ...database config
}),
DoltBranchesModule,
TerminusModule,
],
providers: [BranchResolver],
})
export class AppModule {}
When you make changes to your models or resolvers and run your GraphQL development server
(yarn run dev
), you should see a schema.gql
file updated with the new models and
resolver definitions.
This file is used to generate Typescript object types and hooks that can be used by React.
We build the React portion of this web application using Next.js. Next.js is a React framework that abstracts and automatically configures tooling needed for React, like bundling, compiling, and more.
You can follow these instructions
to install the required packages and create a pages
directory.
Once you have index.tsx
and _app.tsx
components within pages
, you can run the
development server (yarn run dev
). You should see your home page when you navigate to
localhost.
Our Next.js application uses Apollo Client to manage our local and remote data with GraphQL. You can follow these steps to get started.
We created a custom Apollo
wrapper,
which we use to wrap every page component via _app.tsx
.
// pages/_app.tsx
import { withApollo } from "@lib/apollo";
import App from "next/app";
import Head from "next/head";
export default class SampleApp extends App {
public render() {
const { Component } = this.props;
const WrappedPage = withApollo()(Component);
return (
<>
<Head>{/* include various meta tags and scripts here */}</Head>
<WrappedPage {...pageProps} />
</>
);
}
}
We have a page component lists our Dolt branches. From here we can click on a branch to view more branch information or delete a branch.
Each folder or file in the pages
directory creates a new
route. The
branches/index.tsx
file shows a list of branches in our Dolt database and forms the
/branches
route. The branches/[name].tsx
file shows more information about a
particular branch and form the dynamic
route
/branches/[name]
.
// pages/branches/index.tsx
import { NextPage } from "next";
import Link from "next/link";
import BranchList from "../../components/BranchList";
import Page from "../../layouts/page";
const Branches: NextPage = () => {
return (
<Page title="Branches">
<BranchList />
<Link href="/">Back to home</Link>
</Page>
);
};
export default Branches;
The BranchList
component fetches a list of Dolt branches in your database and renders
the branch names. This is the GraphQL
query.
// components/BranchList/queries.ts
import { gql } from "@apollo/client";
export const LIST_BRANCHES = gql`
query ListBranches {
branches {
name
}
}
`;
Since we're using Typescript, we use an additional package called GraphQL Code Generator. This generates Typescript types and React query and mutation hooks based on our GraphQL schema.
After you
install and
configure
the code generator, you can simply run yarn generate-types
to generate code that you'll
find in
gen/graphql-types.tsx
.
We can use the generated useListBranchesQuery
hook to fetch our database's branches.
// components/BranchList/index.tsx
import Link from "next/link";
import ReactLoader from "react-loader";
import { useListBranchesQuery } from "../../gen/graphql-types";
export default function BranchList() {
const res = useListBranchesQuery();
if (res.loading) {
return <ReactLoader loaded={false} />;
}
if (res.error) {
return (
<div className="error-msg">
Error loading branches: {res.error.message}
</div>
);
}
if (!res.data?.branches.length) {
return <div>No branches found</div>;
}
return (
<ul>
{res.data.branches.map((b) => (
<li key={b.name}>
<Link
href="/branches/[name]"
as={`/branches/${encodeURIComponent(b.name)}`}
>
{b.name}
</Link>
</li>
))}
</ul>
);
}