Skip to content

Commit

Permalink
Implement @OneOf input types (#146)
Browse files Browse the repository at this point in the history
* First draft of @OneOf

* Clean up todo

* Add example app using @OneOf

* Add @OneOf to changelog

* Try forcing pnpm version in Netlify

* Set node version in netlify build

* Clean up error message
  • Loading branch information
captbaritone authored Aug 9, 2024
1 parent 86413a5 commit 3d61a2f
Show file tree
Hide file tree
Showing 55 changed files with 1,400 additions and 28 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Changes in this section are not yet released. If you need access to these change

- **Features**
- If a `@gqlType` which is used in an abstract type is defined using an exported `class`, an explicit `__typename` property is no-longer required. Grats can now generate code to infer the `__typename` based on the class definition. (#144)
- Support for `@oneOf` on input types. This allows you to define a discriminated union of input types. (#146)
- **Bug Fixes**
- The experimental TypeScript plugin will now report a diagnostics if it encounters a TypeScript version mismatch. (#143)

Expand Down
1 change: 1 addition & 0 deletions examples/production-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This example includes a relatively fully featured app to demonstrate how real-wo
- Subscriptions - See `Subscription.postLikes` in `models/LikeConnection.ts`
- `@stream` - For expensive lists like `Viewer.feed` in `models/Viewer.ts`
- Custom scalars - See `Date` defined in `graphql/CustomScalars.ts`
- OneOf input types for modeling Markdown content in `models/Post.ts`

## Implementation notes

Expand Down
66 changes: 65 additions & 1 deletion examples/production-app/models/Post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,31 @@ export class Post extends Model<DB.PostRow> implements GraphQLNode {

// --- Mutations ---

/**
* Models a node in a Markdown AST
* @gqlInput
* @oneOf
*/
type MarkdownNode =
| { h1: string }
| { h2: string }
| { h3: string }
| { p: string }
| { blockquote: string }
| { ul: string[] }
| { li: string[] };

/**
* Post content. Could be pure text, or Markdown
* @gqlInput
* @oneOf
*/
type PostContentInput = { string: string } | { markdown: MarkdownNode[] };

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

Expand All @@ -76,6 +97,48 @@ type CreatePostPayload = {
post: Post;
};

// TODO: Use real serialization that handles multiple lines and escapes
// markdown characters.
function serializeMarkdownNode(markdown: MarkdownNode): string {
switch (true) {
case "h1" in markdown:
return `# ${markdown.h1}`;
case "h2" in markdown:
return `## ${markdown.h2}`;
case "h3" in markdown:
return `### ${markdown.h3}`;
case "p" in markdown:
return markdown.p;
case "blockquote" in markdown:
return `> ${markdown.blockquote}`;
case "ul" in markdown:
return markdown.ul.map((item) => `- ${item}`).join("\n");
case "li" in markdown:
return markdown.li.map((item, i) => `${i + 1}. ${item}`).join("\n");
default: {
const _exhaustiveCheck: never = markdown;
throw new Error(`Unexpected markdown node: ${JSON.stringify(markdown)}`);
}
}
}

function serializeMarkdown(markdown: MarkdownNode[]): string {
return markdown.map(serializeMarkdownNode).join("\n");
}

function serializeContent(content: PostContentInput): string {
switch (true) {
case "string" in content:
return content.string;
case "markdown" in content:
return serializeMarkdown(content.markdown);
default: {
const _exhaustiveCheck: never = content;
throw new Error(`Unexpected content: ${JSON.stringify(content)}`);
}
}
}

/**
* Create a new post.
* @gqlField */
Expand All @@ -86,6 +149,7 @@ export async function createPost(
): Promise<CreatePostPayload> {
const post = await DB.createPost(ctx.vc, {
...args.input,
content: serializeContent(args.input.content),
authorId: getLocalTypeAssert(args.input.authorId, "User"),
});
return { post };
Expand Down
2 changes: 1 addition & 1 deletion examples/production-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"dependencies": {
"@graphql-yoga/plugin-defer-stream": "^3.1.1",
"dataloader": "^2.2.2",
"graphql": "16.8.1",
"graphql": "16.9.0",
"graphql-relay": "^0.10.0",
"graphql-yoga": "^5.0.0",
"typescript": "^5.5.4"
Expand Down
19 changes: 18 additions & 1 deletion examples/production-app/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,31 @@ input CreateLikeInput {

input CreatePostInput {
authorId: ID!
content: String!
content: PostContentInput!
title: String!
}

input CreateUserInput {
name: String!
}

"""Models a node in a Markdown AST"""
input MarkdownNode @oneOf {
blockquote: String
h1: String
h2: String
h3: String
li: [String!]
p: String
ul: [String!]
}

"""Post content. Could be pure text, or Markdown"""
input PostContentInput @oneOf {
markdown: [MarkdownNode!]
string: String
}

type CreateLikePayload {
post: Post
}
Expand Down
58 changes: 56 additions & 2 deletions examples/production-app/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,60 @@ export function getSchema(): GraphQLSchema {
};
}
});
const MarkdownNodeType: GraphQLInputObjectType = new GraphQLInputObjectType({
description: "Models a node in a Markdown AST",
name: "MarkdownNode",
fields() {
return {
blockquote: {
name: "blockquote",
type: GraphQLString
},
h1: {
name: "h1",
type: GraphQLString
},
h2: {
name: "h2",
type: GraphQLString
},
h3: {
name: "h3",
type: GraphQLString
},
li: {
name: "li",
type: new GraphQLList(new GraphQLNonNull(GraphQLString))
},
p: {
name: "p",
type: GraphQLString
},
ul: {
name: "ul",
type: new GraphQLList(new GraphQLNonNull(GraphQLString))
}
};
},
isOneOf: true
});
const PostContentInputType: GraphQLInputObjectType = new GraphQLInputObjectType({
description: "Post content. Could be pure text, or Markdown",
name: "PostContentInput",
fields() {
return {
markdown: {
name: "markdown",
type: new GraphQLList(new GraphQLNonNull(MarkdownNodeType))
},
string: {
name: "string",
type: GraphQLString
}
};
},
isOneOf: true
});
const CreatePostInputType: GraphQLInputObjectType = new GraphQLInputObjectType({
name: "CreatePostInput",
fields() {
Expand All @@ -485,7 +539,7 @@ export function getSchema(): GraphQLSchema {
},
content: {
name: "content",
type: new GraphQLNonNull(GraphQLString)
type: new GraphQLNonNull(PostContentInputType)
},
title: {
name: "title",
Expand Down Expand Up @@ -593,6 +647,6 @@ export function getSchema(): GraphQLSchema {
query: QueryType,
mutation: MutationType,
subscription: SubscriptionType,
types: [DateType, NodeType, CreateLikeInputType, CreatePostInputType, CreateUserInputType, CreateLikePayloadType, CreatePostPayloadType, CreateUserPayloadType, LikeType, LikeConnectionType, LikeEdgeType, MutationType, PageInfoType, PostType, PostConnectionType, PostEdgeType, QueryType, SubscriptionType, UserType, UserConnectionType, UserEdgeType, ViewerType]
types: [DateType, NodeType, CreateLikeInputType, CreatePostInputType, CreateUserInputType, MarkdownNodeType, PostContentInputType, CreateLikePayloadType, CreatePostPayloadType, CreateUserPayloadType, LikeType, LikeConnectionType, LikeEdgeType, MutationType, PageInfoType, PostType, PostConnectionType, PostEdgeType, QueryType, SubscriptionType, UserType, UserConnectionType, UserEdgeType, ViewerType]
});
}
Loading

0 comments on commit 3d61a2f

Please sign in to comment.