docs: graphql, schema comments

This commit is contained in:
Vyacheslav Matyukhin 2022-04-22 00:28:39 +04:00
parent db85d80ddb
commit 9a3b7c9d94
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
5 changed files with 105 additions and 20 deletions

45
docs/graphql.md Normal file
View File

@ -0,0 +1,45 @@
Metaforecast website is implemented on top of GraphQL API.
Tech stack:
- [Pothos](https://pothos-graphql.dev/) on the backend for implementing our graphql server
- [urql](https://formidable.com/open-source/urql/) on the frontend
- [GraphQL Code Generator](https://www.graphql-code-generator.com/) for schema generation, queries generation and schema introspection
Rationale for this stack can be found on [#32](https://github.com/quantified-uncertainty/metaforecast/issues/32) and [#21](https://github.com/quantified-uncertainty/metaforecast/issues/32) in comments.
# Code layout
List of all files used for graphql:
- [schema.graphql](../schema.graphql), GraphQL schema generated by graphql-code-generator.
- [codegen.yml](../codegen.yml), graphql-code-generator [config](https://www.graphql-code-generator.com/docs/config-reference/codegen-config)
- [graphql.config.yaml](../graphql.config.yaml), [GraphQL Config](https://www.graphql-config.com/) for better VS Code integration ([VS Code GraphQL extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql) type-checks graphql files thanks to it)
- [src/pages/api/graphql.ts](../src/pages/api/graphql.ts) implements the GraphQL HTTP endpoint and GraphQL playground
- [src/web/urql.ts](../src/web/urql.ts) contains some helper functions.
[src/graphql/](../src/graphql) dir contains GraphQL server code.
`*.graphql` files in `src/web/**` contain GraphQL fragments, queries and mutations which are used on the frontend.
`graphql-code-generator` converts those into `*.generated.ts` files which can be imported from the React components.
# Recipes
**I want to check out what Metaforecast's GraphQL API is capable of**
Go to [/api/graphql](https://metaforecast.org/api/graphql) and do some queries by hand. Note the "Docs" link in the top right corner.
**I want to add a new query/mutation to our schema**
Read the [Pothos](https://pothos-graphql.dev/) docs to learn how to implement new objects and fields. Add the new code somewhere in `src/graphql/schema/`.
**I want to display a new piece of data on the metaforecast website**
- add a query in a nearest `queries.graphql` file (if there isn't one, create it)
- run `gql-gen` (or keep it running with `gql-gen -w`) to get a `graphql.generated.ts` file
- use [useQuery](https://formidable.com/open-source/urql/docs/basics/react-preact/#queries) with your new `MyQueryDocument` which you can import from the `graphql.generated.ts` file
If you need SSR, you'll also need to do **the same** query with **the same variables** from your page's `getServerSideProps` function. Check out any existing page which calls `ssrUrql`.
(You might not need to use `useQuery` in your components if you're doing SSR and the page content is not dynamic, just pass the data from `getServerSideProps` in `props` and avoid GraphQL on the client side).

View File

@ -8,11 +8,18 @@ import { QuestionObj } from "./questions";
const DashboardObj = builder.objectRef<Dashboard>("Dashboard").implement({ const DashboardObj = builder.objectRef<Dashboard>("Dashboard").implement({
fields: (t) => ({ fields: (t) => ({
id: t.exposeID("id"), id: t.exposeID("id"),
title: t.exposeString("title"), title: t.exposeString("title", {
description: t.exposeString("description"), description: "The title of the dashboard",
creator: t.exposeString("creator"), }),
description: t.exposeString("description", {
description: "The longer description of the dashboard",
}),
creator: t.exposeString("creator", {
description: 'The creator of the dashboard, e.g. "Peter Parker"',
}),
questions: t.field({ questions: t.field({
type: [QuestionObj], type: [QuestionObj],
description: "The list of questions on the dashboard",
resolve: async (parent) => { resolve: async (parent) => {
return await prisma.question.findMany({ return await prisma.question.findMany({
where: { where: {
@ -29,6 +36,7 @@ const DashboardObj = builder.objectRef<Dashboard>("Dashboard").implement({
builder.queryField("dashboard", (t) => builder.queryField("dashboard", (t) =>
t.field({ t.field({
type: DashboardObj, type: DashboardObj,
description: "Look up a single dashboard by its id",
args: { args: {
id: t.arg({ type: "ID", required: true }), id: t.arg({ type: "ID", required: true }),
}, },
@ -55,16 +63,25 @@ const CreateDashboardResult = builder
const CreateDashboardInput = builder.inputType("CreateDashboardInput", { const CreateDashboardInput = builder.inputType("CreateDashboardInput", {
fields: (t) => ({ fields: (t) => ({
title: t.string({ required: true }), title: t.string({
description: t.string(), required: true,
creator: t.string(), description: "The title of the dashboard",
ids: t.idList({ required: true }), }),
description: t.string({
description: "The longer description of the dashboard",
}),
creator: t.string({
description: 'The creator of the dashboard, e.g. "Peter Parker"',
}),
ids: t.idList({ required: true, description: "List of question ids" }),
}), }),
}); });
builder.mutationField("createDashboard", (t) => builder.mutationField("createDashboard", (t) =>
t.field({ t.field({
type: CreateDashboardResult, type: CreateDashboardResult,
description:
"Create a new dashboard; if the dashboard with given ids already exists then it will be returned instead.",
args: { args: {
input: t.arg({ type: CreateDashboardInput, required: true }), input: t.arg({ type: CreateDashboardInput, required: true }),
}, },

View File

@ -8,6 +8,7 @@ import { QuestionObj } from "./questions";
builder.queryField("frontpage", (t) => builder.queryField("frontpage", (t) =>
t.field({ t.field({
type: [QuestionObj], type: [QuestionObj],
description: "Get a list of questions that are currently on the frontpage",
resolve: async () => { resolve: async () => {
const legacyQuestions = await getFrontpage(); const legacyQuestions = await getFrontpage();
const ids = legacyQuestions.map((q) => q.id); const ids = legacyQuestions.map((q) => q.id);

View File

@ -3,9 +3,15 @@ import { platforms, QualityIndicators } from "../../backend/platforms";
import { builder } from "../builder"; import { builder } from "../builder";
const PlatformObj = builder.objectRef<string>("Platform").implement({ const PlatformObj = builder.objectRef<string>("Platform").implement({
description: "Platform supported by metaforecast", description: "Forecasting platform supported by Metaforecast",
fields: (t) => ({ fields: (t) => ({
id: t.id({
description: 'Short unique platform name, e.g. "xrisk"',
resolve: (x) => x,
}),
label: t.string({ label: t.string({
description:
'Platform name for displaying on frontend etc., e.g. "X-risk estimates"',
resolve: (platformName) => { resolve: (platformName) => {
if (platformName === "metaforecast") { if (platformName === "metaforecast") {
return "Metaforecast"; return "Metaforecast";
@ -21,9 +27,6 @@ const PlatformObj = builder.objectRef<string>("Platform").implement({
return platform.label; return platform.label;
}, },
}), }),
id: t.id({
resolve: (x) => x,
}),
}), }),
}); });
@ -32,7 +35,9 @@ export const QualityIndicatorsObj = builder
.implement({ .implement({
description: "Various indicators of the question's quality", description: "Various indicators of the question's quality",
fields: (t) => ({ fields: (t) => ({
stars: t.exposeInt("stars"), stars: t.exposeInt("stars", {
description: "0 to 5",
}),
numForecasts: t.int({ numForecasts: t.int({
nullable: true, nullable: true,
resolve: (parent) => resolve: (parent) =>
@ -48,19 +53,28 @@ export const ProbabilityOptionObj = builder
.implement({ .implement({
fields: (t) => ({ fields: (t) => ({
name: t.exposeString("name", { nullable: true }), name: t.exposeString("name", { nullable: true }),
probability: t.exposeFloat("probability", { nullable: true }), // number, 0 to 1 probability: t.exposeFloat("probability", {
description: "0 to 1",
nullable: true,
}),
}), }),
}); });
export const QuestionObj = builder.prismaObject("Question", { export const QuestionObj = builder.prismaObject("Question", {
findUnique: (question) => ({ id: question.id }), findUnique: (question) => ({ id: question.id }),
fields: (t) => ({ fields: (t) => ({
id: t.exposeID("id"), id: t.exposeID("id", {
description: "Unique string which identifies the question",
}),
title: t.exposeString("title"), title: t.exposeString("title"),
description: t.exposeString("description"), description: t.exposeString("description"),
url: t.exposeString("url"), url: t.exposeString("url", {
description:
"Non-unique, a very small number of platforms have a page for more than one prediction",
}),
timestamp: t.field({ timestamp: t.field({
type: "Date", type: "Date",
description: "Timestamp at which metaforecast fetched the question",
resolve: (parent) => parent.timestamp, resolve: (parent) => parent.timestamp,
}), }),
platform: t.field({ platform: t.field({
@ -81,7 +95,7 @@ export const QuestionObj = builder.prismaObject("Question", {
}, },
}), }),
visualization: t.string({ visualization: t.string({
resolve: (parent) => (parent.extra as any)?.visualization, resolve: (parent) => (parent.extra as any)?.visualization, // used for guesstimate only, see searchGuesstimate.ts
nullable: true, nullable: true,
}), }),
}), }),

View File

@ -7,9 +7,15 @@ import { QuestionObj } from "./questions";
const SearchInput = builder.inputType("SearchInput", { const SearchInput = builder.inputType("SearchInput", {
fields: (t) => ({ fields: (t) => ({
query: t.string({ required: true }), query: t.string({ required: true }),
starsThreshold: t.int(), starsThreshold: t.int({
forecastsThreshold: t.int(), description: "Minimum number of stars on a question",
forecastingPlatforms: t.stringList(), }),
forecastsThreshold: t.int({
description: "Minimum number of forecasts on a question",
}),
forecastingPlatforms: t.stringList({
description: "List of platform ids to filter by",
}),
limit: t.int(), limit: t.int(),
}), }),
}); });
@ -17,13 +23,15 @@ const SearchInput = builder.inputType("SearchInput", {
builder.queryField("searchQuestions", (t) => builder.queryField("searchQuestions", (t) =>
t.field({ t.field({
type: [QuestionObj], type: [QuestionObj],
description:
"Search for questions; uses Algolia instead of the primary metaforecast database",
args: { args: {
input: t.arg({ type: SearchInput, required: true }), input: t.arg({ type: SearchInput, required: true }),
}, },
resolve: async (parent, { input }) => { resolve: async (parent, { input }) => {
// defs // defs
const query = input.query === undefined ? "" : input.query; const query = input.query === undefined ? "" : input.query;
if (query == "") return []; if (query === "") return [];
const forecastsThreshold = input.forecastsThreshold; const forecastsThreshold = input.forecastsThreshold;
const starsThreshold = input.starsThreshold; const starsThreshold = input.starsThreshold;
const platformsIncludeGuesstimate = const platformsIncludeGuesstimate =