Skip to content

Commit

Permalink
feat: add timestamp to model filter (#234)
Browse files Browse the repository at this point in the history
* feat: add timestamp to model filter

* fix: lint

* test: add timestamp tests for graphql server

* test: add derived field test, currently failing

* wip, timestamps not propagating

* fix type

* skip failing test and add comment

* add test for arguments to derived field

* chore: changeset

* update docs

---------

Co-authored-by: typedarray <[email protected]>
  • Loading branch information
r0ohafza and 0xOlias committed Jul 21, 2023
1 parent c5b5b8e commit 0e90313
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 219 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-stingrays-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ponder/core": patch
---

Added support for passing arguments to derived fields. This means you can paginate entities returned in a derived field. Also added support for time-travel queries via the `timestamp` argument to all GraphQL root query types. NOTE: There is currently a limitation where `timestamp` arguments are not automatically passed to derived fields. If you are using time-travel queries on entities with derived fields, be sure the pass the same `timestamp` as an argument to the derived field. This will be fixed in a future release.
102 changes: 93 additions & 9 deletions docs/pages/guides/query-the-graphql-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ type Person @entity {
{/* prettier-ignore */}
```graphql filename="Generated schema"
type Query {
person(id: Int!): Person
person(id: Int!, timestamp: Int): Person

persons(
where: PersonFilter
first: Int
skip: Int
orderBy: String
orderDirection: String
skip: Int = 0
first: Int = 100
orderBy: String = "id"
orderDirection: String = "asc"
where: PersonFilter,
timestamp: Int
): [Person!]!
}
```
Expand All @@ -47,7 +49,7 @@ type Query {

## Filtering

The GraphQL API supports filtering through the `where` arguments. The `where` argument is an object that contains filter options for each field you want to filter on. Here are the filter options available for each field type.
The GraphQL API supports filtering through the `where` argument. The `where` argument type contains filter options for every field defined on your entity. Here are the filter options available for each field type.

| Filter option | Available for field types | _Include entities where..._ |
| :------------------------ | :--------------------------------------- | :--------------------------------------------------------------- |
Expand All @@ -66,7 +68,7 @@ The GraphQL API supports filtering through the `where` arguments. The `where` ar
| `{field}_ends_with` | String scalars (String, Bytes) | \{field\} is **ends with** specified value |
| `{field}_not_ends_with` | String scalars (String, Bytes) | \{field\} is **does not end with** specified value |

For the following examples, assume these entities exist in your database.
For all following examples, assume these entities exist in your database.

<div className="code-columns">

Expand Down Expand Up @@ -179,7 +181,7 @@ query {

## Sorting

The GraphQL API supports sorting through the `orderBy` and `orderDirection` arguments.
Use the `orderBy` and `orderDirection` arguments to sort entities by a scalar field. String scalars (String, Bytes) use a lexicographic sort.

| Pagination option | Default |
| :---------------- | :------ |
Expand Down Expand Up @@ -211,3 +213,85 @@ query {
```

</div>

## Time-travel queries

Using time-travel queries, you can query the state of your app's database at any point in history. To construct a time-travel query, pass a Unix timestamp to the `timestamp` argument on any of the root query types.

| Time-travel option | Default |
| :----------------- | :--------------------- |
| `timestamp` | `undefined` ("latest") |

In this example, consider that only Pablo had been added to the database at the specified timestamp, and his age at that time was 42. The other entities were inserted later.

<div className="code-columns">

{/* prettier-ignore */}
```graphql filename="Query"
query {
persons(timestamp: 1689910567) {
name
age
}
}
```

{/* prettier-ignore */}
```json filename="Result"
{
"persons": [
{ "name": "Pablo", "age": 42 },
]
}
```

</div>

## Derived fields

Derived fields return the list of child/derived entities that "belong" to the parent. They're very similar to the plural query field that, except derived fields are automatically filtered by the parent's ID.

<div className="code-columns">

{/* prettier-ignore */}
```graphql filename="schema.graphql"
type Person @entity {
id: Int!
pets: [Pet!]! @derivedFrom(field: "ownerId")
}

type Pet @entity {
id: Int!
name: String!
ownerId: Int!
}
```

{/* prettier-ignore */}
```graphql filename="Generated schema"
type Query {
# ...
persons(
skip: Int = 0
first: Int = 100
orderBy: String = "id"
orderDirection: String = "asc"
where: PersonFilter,
timestamp: Int
): [Person!]!
}

type Person {
id: Int!
pets(
skip: Int = 0
first: Int = 100
orderBy: String = "id"
orderDirection: String = "asc"
where: PetFilter, # This automatically has { ownerId: person.id } applied
timestamp: Int
): [Pet!]!
}
```

</div>
2 changes: 1 addition & 1 deletion packages/core/src/codegen/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class CodegenService extends Emittery {
`;

const body = printSchema(graphqlSchema);
const final = header + body;
const final = formatPrettier(header + body, { parser: "graphql" });

const filePath = path.join(
this.resources.options.generatedDir,
Expand Down
20 changes: 17 additions & 3 deletions packages/core/src/server/graphql/entity.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
GraphQLFieldConfigMap,
GraphQLFieldResolver,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLOutputType,
GraphQLString,
} from "graphql";

import type { Entity } from "@/schema/types";
Expand Down Expand Up @@ -91,13 +93,18 @@ export const buildEntityType = ({
// @ts-ignore
const entityId = parent.id;

const filter = args;

return await store.findMany({
modelName: field.derivedFromEntityName,
filter: {
where: {
[field.derivedFromFieldName]: entityId,
},
where: { [field.derivedFromFieldName]: entityId },
skip: filter.skip,
first: filter.first,
orderBy: filter.orderBy,
orderDirection: filter.orderDirection,
},
timestamp: filter.timestamp ? filter.timestamp : undefined,
});
};

Expand All @@ -107,6 +114,13 @@ export const buildEntityType = ({
new GraphQLNonNull(entityGqlTypes[field.baseGqlType.name])
)
),
args: {
skip: { type: GraphQLInt, defaultValue: 0 },
first: { type: GraphQLInt, defaultValue: 100 },
orderBy: { type: GraphQLString, defaultValue: "id" },
orderDirection: { type: GraphQLString, defaultValue: "asc" },
timestamp: { type: GraphQLInt },
},
resolve: resolver,
};

Expand Down
22 changes: 17 additions & 5 deletions packages/core/src/server/graphql/plural.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type PluralArgs = {
skip?: number;
orderBy?: string;
orderDirection?: "asc" | "desc";
timestamp?: number;
};
type PluralResolver = GraphQLFieldResolver<Source, Context, PluralArgs>;

Expand Down Expand Up @@ -163,19 +164,30 @@ const buildPluralField = ({

const filter = args;

return await store.findMany({ modelName: entity.name, filter });
return await store.findMany({
modelName: entity.name,
filter: {
skip: filter.skip,
first: filter.first,
orderBy: filter.orderBy,
orderDirection: filter.orderDirection,
where: filter.where,
},
timestamp: filter.timestamp ? filter.timestamp : undefined,
});
};

return {
type: new GraphQLNonNull(
new GraphQLList(new GraphQLNonNull(entityGqlType))
),
args: {
skip: { type: GraphQLInt, defaultValue: 0 },
first: { type: GraphQLInt, defaultValue: 100 },
orderBy: { type: GraphQLString, defaultValue: "id" },
orderDirection: { type: GraphQLString, defaultValue: "asc" },
where: { type: filterType },
first: { type: GraphQLInt },
skip: { type: GraphQLInt },
orderBy: { type: GraphQLString },
orderDirection: { type: GraphQLString },
timestamp: { type: GraphQLInt },
},
resolve: resolver,
};
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/server/graphql/singular.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
GraphQLFieldConfig,
GraphQLFieldResolver,
GraphQLInt,
GraphQLNonNull,
GraphQLObjectType,
} from "graphql";
Expand All @@ -11,6 +12,7 @@ import type { Context, Source } from "./schema";

type SingularArgs = {
id?: string;
timestamp?: number;
};
type SingularResolver = GraphQLFieldResolver<Source, Context, SingularArgs>;

Expand All @@ -23,13 +25,14 @@ const buildSingularField = ({
}): GraphQLFieldConfig<Source, Context> => {
const resolver: SingularResolver = async (_, args, context) => {
const { store } = context;
const { id } = args;
const { id, timestamp } = args;

if (!id) return null;

const entityInstance = await store.findUnique({
modelName: entity.name,
id: id,
id,
timestamp,
});

return entityInstance;
Expand All @@ -39,6 +42,7 @@ const buildSingularField = ({
type: entityGqlType,
args: {
id: { type: new GraphQLNonNull(entity.fieldByName.id.scalarGqlType) },
timestamp: { type: GraphQLInt },
},
resolve: resolver,
};
Expand Down
Loading

1 comment on commit 0e90313

@vercel
Copy link

@vercel vercel bot commented on 0e90313 Jul 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.