-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
05c9a8d
commit 3799fd3
Showing
14 changed files
with
5,316 additions
and
1,186 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)!); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** @gqlType */ | ||
export type Query = unknown; | ||
|
||
/** @gqlType */ | ||
export type Mutation = unknown; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:*" | ||
} | ||
} |
Oops, something went wrong.