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

feat: add timestamp to model filter #234

Merged
merged 10 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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