Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: /docs/reference/graphql #875

Merged
merged 17 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions docs/metatype.dev/docs/reference/graphql/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
---
sidebar_position: 50
---

import TGExample from "@site/src/components/TGExample";

# GraphQL

[GraphQL](https://graphql.org/) is the primary means of querying your typegraph.
This page documents all the semantics of how your typegraph translates into a GraphQL schema.

## `Query` and `Mutation`

The root functions passed to the `expose` function will be added as fields to the [special `query` and `mutation`](https://graphql.org/learn/schema/#the-query-and-mutation-types) GraphQL objects.
Under which object a function is assigned depends on it's [effect](/docs/reference/types/functions).
Note that this assignment still holds for deeply nested functions so be sure to avoid including functions that have mutating effects under query responses or vice versa.

## Variables

While [GraphQL Variables](https://graphql.org/learn/queries/#variables) work as expected for the most part, the typegate currently does not do type validation of variables for the provided query documents.
Natoandro marked this conversation as resolved.
Show resolved Hide resolved

```graphql
query ($varRight: String!, $varWrong: Int!) {
foo1: foo(in: $varRight)
foo2: foo(in: $varWrong)
}
{
"varRight": "string",
# request will work as long as varWrong is string
# even if variable above was annotated as Int
"varWrong": "AlsoString",
}
```

## Types

### Scalars

The simple primitive typegraph types translate directly to their [GraphQL equivalent](https://graphql.org/learn/schema/#scalar-types).
This includes standard scalar variants like `t.uuid` or any user declared aliases with custom configurations.
No custom GraphQL scalars are generated for such types.


| Type | GraphQL type | Description |
| -------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------- |
| `t.integer()` | `Int` | Represents signed 32-bit integers. |
| `t.float()` | `Float` | Represents signed double-precision values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754). |
| `t.boolean()` | `Boolean` | Represents `true` or `false`. |
| `t.string()` | `String` | Represents textual data as UTF-8 character sequences. |

### Files

File types, primarily used through the [`S3Runtime`](/docs/reference/runtimes/s3), translate to a custom scalar called `File` in the GraphQL schema.
The `S3Runtime` provides a set of helpers for all your file needs including support for [multipart HTTP requests](/docs/guides/files-upload) for file upload.

### Structs

`t.structs` types translate to:
- [GraphQL input types](https://graphql.org/learn/schema/#input-types) when used as inputs to functions
- [GraphQL object types](https://graphql.org/learn/schema/#object-types-and-fields) in any other place.

### Lists

`t.list` types simply convert to [GraphQL list types](https://graphql.org/learn/schema/#lists-and-non-null).

### Functions

`t.func` types are represented by their output type in the GraphQL schema.
If their input struct has fields present, these are converted to [arguments](https://graphql.org/learn/schema/#arguments) for the field.

### Unions and Eithers

Unions and either translate to a number of different forms depending on their usage in the graph and their composition.

When used as a field in an input struct, unions/eithers are converted to custom scalars since the GraphQL spec doesn't allow GraphQL unions on input types.
These scalars expect the standard JSON representation of your value and that is all.

When querying union/either values, there are a few more details.
For struct variants, the standard GraphQL rules apply and [inline fragments](https://graphql.org/learn/queries/#inline-fragments) must be used.
Currently, all struct variants must have inline fragments present even if the user is not interested in them.
And as per GraphQL spec, common field querying is not supported and any fields must be part of the inline fragment.
Natoandro marked this conversation as resolved.
Show resolved Hide resolved
Unlike the GraphQL spec, this includes the `__typename` field which must be included in the fragments instead.

```graphql
# introspection schema
union MyUnion = Struct1 | Struct2

type Struct1 {
foo: String!
bar: String!
}

type Struct2 {
foo: String!
baz: String!
}

# introspection expected query (THIS IS WRONG)
query {
myUnion {
# common fields are not supported
__typename
foo
... on Struct1 {
bar
}
... on Struct2 {
baz
}
}
}

# actual expected query (THIS IS RIGHT)
query {
myUnion {
... on Struct1 {
# common fields must be included each time
__typename
foo
bar
}
... on Struct2 {
__typename
foo
baz
}
}
}
```

For scalar variant, the introspected GraphQL schema will include a `_NameOfScalar` variant in the introspection schema **but** the value is returned at the field level as a simple scalar.
That is, GraphQL API explorers will show union __object__ members that include the scalar values and they'll prompt you to use [inline fragments](https://graphql.org/learn/queries/#inline-fragments) for querying the scalar members.
But the typegate will reject these kind of queries in preference to simple fields.
Look at the following example:

```graphql
# introspection schema
union MyUnion = _String | _Integer

type _String {
string: String!
}

type _Integer {
integer: Int!
}

# introspection expected query (THIS IS WRONG)
query {
myUnion {
... on _String {
string
}
... on _Integer {
integer
}
}
}

# actual expected query (THIS IS RIGHT)
query {
# no subfield selection at all required
# since all members are scalars
myUnion
}

# received json
{
"myUnion": "string"
}
```

List members act accordingly to their wrapped times.
Lists of scalars are treated as scalars and lists of composites as a standard GraphQL composites list.

The following playground shows all the different types of unions/eithers:

<TGExample
typegraph="union-either"
typescript={require("!!code-loader!../../../../../examples/typegraphs/union-either.ts")}
python={require("!!code-loader!../../../../../examples/typegraphs/union-either.py")}
query={require("./union-either.graphql")}
/>
33 changes: 33 additions & 0 deletions docs/metatype.dev/docs/reference/graphql/union-either.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
outer {
# scalar 1 and 2 don't need
# type inline fragments
unionList {
... on comp_1 {
field1
}
... on comp_2 {
field2
}
}
# we must include fragments
# for all composites or it will fail
union {
... on comp_1 {
field1
}
... on comp_2 {
field2
}
}
# rules are the same between unions and eithers
either {
... on comp_1 {
field1
}
... on comp_2 {
field2
}
}
}
}
5 changes: 3 additions & 2 deletions examples/typegraphs/triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { HttpRuntime } from "@typegraph/sdk/runtimes/http.ts";
typegraph(
{
name: "triggers",
// skip:next-line
cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] },
},
(g) => {
Expand All @@ -21,7 +22,7 @@ typegraph(
path: "/flip_coin",
}),
},
pub
pub,
);
}
},
);
69 changes: 69 additions & 0 deletions examples/typegraphs/union-either.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# skip:start
from typegraph import typegraph, Policy, t, Graph
from typegraph.graph.params import Cors
from typegraph.runtimes.deno import DenoRuntime


# skip:end
@typegraph(
# skip:start
cors=Cors(allow_origin=["https://metatype.dev", "http://localhost:3000"]),
# skip:end
)
def union_either(g: Graph):
deno = DenoRuntime()
members = [
t.string().rename("scalar_1"),
t.integer().rename("scalar_2"),
t.struct(
{
"field1": t.string(),
}
).rename("comp_1"),
t.struct(
{
"field2": t.string(),
}
).rename("comp_2"),
t.list(t.string()).rename("scalar_list"),
# # FIXME: list of composites is broken
# t.list(
# t.struct(
# {
# "listField": t.string(),
# }
# ),
# ),
]
g.expose(
Policy.public(),
outer=deno.func(
t.struct(),
t.struct(
{
"unionList": t.list(t.union(members)),
"union": t.union(members),
"either": t.either(members),
}
),
code="""() => ({
unionList: [
"scalar",
2,
{
field1: "1",
},
{
field2: "2",
},
["scalar_1", "scalar_2"],
],
either: {
field1: "1",
},
union: {
field2: "2",
},
})""",
),
)
65 changes: 65 additions & 0 deletions examples/typegraphs/union-either.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// skip:start
import { Policy, t, typegraph } from "@typegraph/sdk/index.ts";
import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.ts";

// skip:end
typegraph(
{
name: "union-either",
// skip:next-line
cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] },
},
(g) => {
const deno = new DenoRuntime();
const members = [
t.string().rename("scalar_1"),
t.integer().rename("scalar_2"),
t.struct({
field1: t.string(),
}).rename("comp_1"),
t.struct({
field2: t.string(),
}).rename("comp_2"),
t.list(t.string()).rename("scalar_list"),
/* FIXME: list of composites is broken
t.list(
t.struct({
listField: t.string(),
}),
), */
];
g.expose({
outer: deno.func(
// input
t.struct({}),
// output
t.struct({
union: t.union(members),
either: t.either(members),
unionList: t.list(t.union(members)),
}),
{
code: () => ({
either: {
field1: "1",
},
union: {
field2: "2",
},
unionList: [
"scalar",
2,
{
field1: "1",
},
{
field2: "2",
},
["scalar_1", "scalar_2"],
],
}),
},
),
}, Policy.public());
},
);
2 changes: 1 addition & 1 deletion src/typegraph/deno/src/typegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export async function typegraph(
if (err.payload && !err.cause) {
err.cause = err.payload;
}
if ("stack" in err.cause) {
if (err.cause && "stack" in err.cause) {
console.error(`Error in typegraph '${name}':`);
for (const msg of err.cause.stack) {
console.error(`- ${msg}`);
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/cli/dev_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ Meta.test({
["roadmap-policies.ts", "roadmap-policies"],
["roadmap-random.ts", "roadmap-random"],
["triggers.ts", "triggers"],
["union-either.ts", "union-either"],
]);
});
});
1 change: 1 addition & 0 deletions tests/e2e/website/website_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const list = [
"roadmap-random",
"temporal",
"triggers",
"union-either",
] as const;

// files that does not have 2 versions -- TODO why?
Expand Down
Loading
Loading