Skip to content

Commit

Permalink
Initial version of advanced example
Browse files Browse the repository at this point in the history
  • Loading branch information
captbaritone committed Mar 7, 2024
1 parent 05c9a8d commit 3799fd3
Show file tree
Hide file tree
Showing 14 changed files with 5,316 additions and 1,186 deletions.
108 changes: 108 additions & 0 deletions examples/advanced/Database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { VC } from "./ViewerContext";

/**
* This module is intended to represent a database.
*
* Note that we purposefully thead the VC all the way through to the database
* layer so that we can do permission checks and access logging at the lowest
* possible layer
*/
export type UserRow = {
id: string;
name: string;
};

export type PostRow = {
id: string;
authorId: string;
title: string;
content: string;
};

const MOCK_USERS: UserRow[] = [
{ id: "1", name: "Alice" },
{ id: "2", name: "Bob" },
{ id: "3", name: "Charlie" },
];

const MOCK_POSTS: PostRow[] = [
{
id: "1",
authorId: "1",
title: "Hello, World!",
content: "This is my first post.",
},
{
id: "2",
authorId: "2",
title: "My favorite things",
content: "Here are some things I like.",
},
{
id: "3",
authorId: "3",
title: "I'm back",
content: "I was away for a while.",
},
{
id: "4",
authorId: "1",
title: "Hello again",
content: "I'm back too.",
},
{
id: "5",
authorId: "2",
title: "My favorite things 2",
content: "Here are some more things I like.",
},
];

export async function selectPosts(vc: VC): Promise<Array<PostRow>> {
vc.log("DB query: selectPosts");
return MOCK_POSTS;
}

export async function createPost(
vc: VC,
draft: {
authorId: string;
title: string;
content: string;
},
): Promise<PostRow> {
vc.log(`DB query: createPost: ${JSON.stringify(draft)}`);
const id = (MOCK_POSTS.length + 1).toString();
const row = { id, ...draft };
MOCK_POSTS.push(row);
return row;
}

export async function selectPostsWhereAuthor(
vc: VC,
authorId: string,
): Promise<Array<PostRow>> {
vc.log(`DB query: selectPostsWhereAuthor: ${authorId}`);
return MOCK_POSTS.filter((post) => post.authorId === authorId);
}

export async function selectUsers(vc: VC): Promise<Array<UserRow>> {
vc.log("DB query: selectUsers");
return MOCK_USERS;
}

export async function getPostsByIds(
vc: VC,
ids: readonly string[],
): Promise<Array<PostRow>> {
vc.log(`DB query: getPostsByIds: ${ids.join(", ")}`);
return ids.map((id) => MOCK_POSTS.find((post) => post.id === id)!);
}

export async function getUsersByIds(
vc: VC,
ids: readonly string[],
): Promise<Array<UserRow>> {
vc.log(`DB query: getUsersByIds: ${ids.join(", ")}`);
return ids.map((id) => MOCK_USERS.find((user) => user.id === id)!);
}
15 changes: 15 additions & 0 deletions examples/advanced/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Advanced Example

This example includes a relatively fully featured app to demonstrate how real-world applications can be built with Grats.

## Features

- [Node interface](https://graphql.org/learn/global-object-identification/)
- [dataloader](https://github.com/graphql/dataloader)
- _TODO_: Connections
- _TODO_: Custom scalars

## Running the demo

- `$ pnpm install`
- `$ pnpm run start`
35 changes: 35 additions & 0 deletions examples/advanced/ViewerContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import DataLoader from "dataloader";
import { PostRow, UserRow, getPostsByIds, getUsersByIds } from "./Database";
import { YogaInitialContext } from "graphql-yoga";

/**
* Viewer Context
*
* This object represents the entity reading the data during this request. It
* acts as a per-request memoization cache as well as a representation of what
* permissions the entity reading the data has.
*
* It should be constructed once at the beginning of the request and threaded
* through the entire request.
*/
export class VC {
_postLoader: DataLoader<string, PostRow>;
_userLoader: DataLoader<string, UserRow>;
_logs: string[] = [];
constructor() {
this._postLoader = new DataLoader((ids) => getPostsByIds(this, ids));
this._userLoader = new DataLoader((ids) => getUsersByIds(this, ids));
}
async getPostById(id: string): Promise<PostRow> {
return this._postLoader.load(id);
}
async getUserById(id: string): Promise<UserRow> {
return this._userLoader.load(id);
}
log(message: string) {
this._logs.push(message);
console.log(message);
}
}

export type Ctx = YogaInitialContext & { vc: VC };
68 changes: 68 additions & 0 deletions examples/advanced/graphql/Node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ID } from "grats";
import { Query } from "./Roots";
import { Ctx } from "../ViewerContext";
import { User } from "../models/User";
import { Post } from "../models/Post";

/**
* Indicates a stable refetchable object in the system.
* @gqlInterface Node */
export interface GraphQLNode {
__typename: string;
localID(): string;
}

/**
* A globally unique opaque identifier for a node. Can be used to fetch the the
* node with the `node` or `nodes` fields.
*
* See: https://graphql.org/learn/global-object-identification/
*
* @gqlField
* @killsParentOnExceptions */
export function id(node: GraphQLNode): ID {
return encodeID(node.__typename, node.localID());
}

/**
* Fetch a single `Node` by its globally unique ID.
* @gqlField */
export async function node(
_: Query,
args: { id: ID },
ctx: Ctx,
): Promise<GraphQLNode | null> {
const { typename, localID } = decodeID(args.id);

// Note: Every type which implements `Node` must be represented here, and
// there's not currently any static way to enforce that. This is a potential
// source of bugs.
switch (typename) {
case "User":
return new User(await ctx.vc.getUserById(localID));
case "Post":
return new Post(await ctx.vc.getPostById(localID));
default:
throw new Error(`Unknown typename: ${typename}`);
}
}

/**
* Fetch a list of `Node`s by their globally unique IDs.
* @gqlField */
export async function nodes(
_: Query,
args: { ids: ID[] },
ctx: Ctx,
): Promise<Array<GraphQLNode | null>> {
return Promise.all(args.ids.map((id) => node(_, { id }, ctx)));
}

export function encodeID(typename: string, id: string): ID {
return btoa(typename + ":" + id);
}

export function decodeID(id: ID): { typename: string; localID: string } {
const [typename, localID] = atob(id).split(":");
return { typename, localID };
}
5 changes: 5 additions & 0 deletions examples/advanced/graphql/Roots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @gqlType */
export type Query = unknown;

/** @gqlType */
export type Mutation = unknown;
9 changes: 9 additions & 0 deletions examples/advanced/models/Model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Generic model class built around a database row
*/
export abstract class Model<R extends { id: string }> {
constructor(protected row: R) {}
localID(): string {
return this.row.id;
}
}
64 changes: 64 additions & 0 deletions examples/advanced/models/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as DB from "../Database";
import { Ctx } from "../ViewerContext";
import { GraphQLNode } from "../graphql/Node";
import { User } from "./User";
import { Model } from "./Model";
import { Mutation, Query } from "../graphql/Roots";
import { ID } from "../../../dist/src";

/**
* A blog post.
* @gqlType */
export class Post extends Model<DB.PostRow> implements GraphQLNode {
__typename = "Post";

/**
* The editor-approved title of the post.
* @gqlField */
title(): string {
return this.row.title;
}

/**
* Content of the post in markdown.
* @gqlField */
content(): string {
return this.row.content;
}

/**
* The author of the post. This cannot change after the post is created.
* @gqlField */
async author(_args: unknown, ctx: Ctx): Promise<User> {
return new User(await ctx.vc.getUserById(this.row.authorId));
}
}

// --- Root Fields ---

/**
* All posts in the system. Note that there is no guarantee of order.
* @gqlField */
export async function posts(_: Query, __: unknown, ctx: Ctx): Promise<Post[]> {
const rows = await DB.selectPosts(ctx.vc);
return rows.map((row) => new Post(row));
}

/** @gqlInput */
type CreatePostInput = {
title: string;
content: string;
authorId: ID;
};

/**
* Create a new post.
* @gqlField */
export async function createPost(
_: Mutation,
args: { input: CreatePostInput },
ctx: Ctx,
): Promise<Post> {
const row = await DB.createPost(ctx.vc, args.input);
return new Post(row);
}
36 changes: 36 additions & 0 deletions examples/advanced/models/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as DB from "../Database";
import { Ctx } from "../ViewerContext";
import { GraphQLNode } from "../graphql/Node";
import { Query } from "../graphql/Roots";
import { Model } from "./Model";
import { Post } from "./Post";

/** @gqlType */
export class User extends Model<DB.UserRow> implements GraphQLNode {
__typename = "User";

/**
* User's name. **Note:** This field is not guaranteed to be unique.
* @gqlField */
name(): string {
return this.row.name;
}

/**
* All posts written by this user. Note that there is no guarantee of order.
* @gqlField */
async posts(_: unknown, ctx: Ctx): Promise<Post[]> {
const rows = await DB.selectPostsWhereAuthor(ctx.vc, this.row.id);
return rows.map((row) => new Post(row));
}
}

// --- Root Fields ---

/**
* All users in the system. Note that there is no guarantee of order.
* @gqlField */
export async function users(_: Query, __: unknown, ctx: Ctx): Promise<User[]> {
const rows = await DB.selectUsers(ctx.vc);
return rows.map((row) => new User(row));
}
37 changes: 37 additions & 0 deletions examples/advanced/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "grats-example-yoga",
"version": "0.0.0",
"description": "Example server showcasing Grats used with Yoga",
"main": "index.js",
"scripts": {
"start": "tsc && node dist/server.js",
"grats": "grats",
"build": "tsc"
},
"repository": {
"type": "git",
"url": "git+https://github.com/captbaritone/grats.git"
},
"keywords": [
"graphql",
"grats",
"yoga",
"javascript",
"typescript",
"subscriptions"
],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/captbaritone/grats/issues"
},
"homepage": "https://github.com/captbaritone/grats#readme",
"dependencies": {
"dataloader": "^2.2.2",
"graphql-yoga": "^5.0.0"
},
"devDependencies": {
"@types/node": "^20.8.10",
"grats": "workspace:*"
}
}

0 comments on commit 3799fd3

Please sign in to comment.