docs: graphql, schema comments
This commit is contained in:
parent
db85d80ddb
commit
9a3b7c9d94
45
docs/graphql.md
Normal file
45
docs/graphql.md
Normal 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).
|
|
@ -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 }),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
Loading…
Reference in New Issue
Block a user