Skip to content

Commit

Permalink
Replace implementation of the lists API (#6153)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmatown authored Jul 21, 2021
1 parent 6d70ae1 commit 7716315
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 663 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-berries-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/keystone': major
---

Removed implicit chunking from the lists API so that the lists API is a direct translation of the GraphQL API
43 changes: 2 additions & 41 deletions packages/keystone/src/lib/coerceAndValidateForGraphQLInput.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,6 @@
import {
GraphQLSchema,
VariableDefinitionNode,
TypeNode,
GraphQLType,
GraphQLNonNull,
GraphQLList,
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLObjectType,
GraphQLScalarType,
GraphQLUnionType,
ListTypeNode,
NamedTypeNode,
GraphQLInputType,
GraphQLError,
} from 'graphql';
import { GraphQLSchema, VariableDefinitionNode, GraphQLInputType, GraphQLError } from 'graphql';
import { getVariableValues } from 'graphql/execution/values';

function getNamedOrListTypeNodeForType(
type:
| GraphQLScalarType
| GraphQLObjectType<any, any>
| GraphQLInterfaceType
| GraphQLUnionType
| GraphQLEnumType
| GraphQLInputObjectType
| GraphQLList<any>
): NamedTypeNode | ListTypeNode {
if (type instanceof GraphQLList) {
return { kind: 'ListType', type: getTypeNodeForType(type.ofType) };
}
return { kind: 'NamedType', name: { kind: 'Name', value: type.name } };
}

function getTypeNodeForType(type: GraphQLType): TypeNode {
if (type instanceof GraphQLNonNull) {
return { kind: 'NonNullType', type: getNamedOrListTypeNodeForType(type.ofType) };
}
return getNamedOrListTypeNodeForType(type);
}
import { getTypeNodeForType } from './context/executeGraphQLFieldToRootVal';

const argName = 'where';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ function getNamedOrListTypeNodeForType(
return { kind: 'NamedType', name: { kind: 'Name', value: type.name } };
}

function getTypeNodeForType(type: GraphQLType): TypeNode {
export function getTypeNodeForType(type: GraphQLType): TypeNode {
if (type instanceof GraphQLNonNull) {
return { kind: 'NonNullType', type: getNamedOrListTypeNodeForType(type.ofType) };
}
return getNamedOrListTypeNodeForType(type);
}

function getVariablesForGraphQLField(field: GraphQLField<any, any>) {
export function getVariablesForGraphQLField(field: GraphQLField<any, any>) {
const variableDefinitions: VariableDefinitionNode[] = field.args.map(arg => ({
kind: 'VariableDefinition',
type: getTypeNodeForType(arg.type),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
execute,
FragmentDefinitionNode,
GraphQLList,
GraphQLNonNull,
GraphQLOutputType,
GraphQLSchema,
parse,
validate,
} from 'graphql';
import { KeystoneContext } from '@keystone-next/types';
import { getVariablesForGraphQLField } from './executeGraphQLFieldToRootVal';

function getRootTypeName(type: GraphQLOutputType): string {
if (type instanceof GraphQLNonNull) {
return getRootTypeName(type.ofType);
}
if (type instanceof GraphQLList) {
return getRootTypeName(type.ofType);
}
return type.name;
}

export function executeGraphQLFieldWithSelection(
schema: GraphQLSchema,
operation: 'query' | 'mutation',
fieldName: string
) {
const rootType = operation === 'mutation' ? schema.getMutationType()! : schema.getQueryType()!;
const field = rootType.getFields()[fieldName];
if (field === undefined) {
return () => {
throw new Error('You do not have access to this resource');
};
}
const { argumentNodes, variableDefinitions } = getVariablesForGraphQLField(field);
const rootName = getRootTypeName(field.type);
return async (args: Record<string, any>, query: string, context: KeystoneContext) => {
const selectionSet = (
parse(`fragment x on ${rootName} {${query}}`).definitions[0] as FragmentDefinitionNode
).selectionSet;

const document = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation,
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: field.name },
arguments: argumentNodes,
selectionSet: selectionSet,
},
],
},
variableDefinitions,
},
],
} as const;

const validationErrors = validate(schema, document);

if (validationErrors.length > 0) {
throw validationErrors[0];
}

const result = await execute({
schema,
document,
contextValue: context,
variableValues: Object.fromEntries(
// GraphQL for some reason decides to make undefined values in args
// skip defaulting for some reason
// this ofc doesn't technically fully fix it (bc nested things)
// but for the cases where we care, it does
Object.entries(args).filter(([, val]) => val !== undefined)
),
rootValue: {},
});
if (result.errors?.length) {
throw result.errors[0];
}
return result.data![field.name];
};
}
103 changes: 26 additions & 77 deletions packages/keystone/src/lib/context/itemAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,8 @@ import {
KeystoneContext,
GqlNames,
} from '@keystone-next/types';
import {
getItem,
getItems,
createItem,
createItems,
updateItem,
updateItems,
deleteItem,
deleteItems,
} from './server-side-graphql-client';
import { executeGraphQLFieldToRootVal } from './executeGraphQLFieldToRootVal';
import { executeGraphQLFieldWithSelection } from './executeGraphQLFieldWithSelection';

// this is generally incorrect because types are open in TS but is correct in the specific usage here.
// (i mean it's not really any more incorrect than TS is generally is but let's ignore that)
Expand Down Expand Up @@ -83,83 +74,41 @@ export function itemAPIForList(
context: KeystoneContext,
dbAPI: KeystoneDbAPI<Record<string, BaseGeneratedListTypes>>[string]
): KeystoneListsAPI<Record<string, BaseGeneratedListTypes>>[string] {
return {
findOne({ query, resolveFields, ...args }) {
const returnFields = defaultQueryParam(query, resolveFields);
if (returnFields) {
return getItem({ listKey, context, returnFields, where: args.where });
} else {
return dbAPI.findOne(args);
}
},
findMany({ query, resolveFields, ...args } = {}) {
const f = (
operation: 'query' | 'mutation',
field: string,
dbAPIVersionOfAPI: (args: any) => Promise<any>
) => {
const exec = executeGraphQLFieldWithSelection(context.graphql.schema, operation, field);
return ({
query,
resolveFields,
...args
}: { resolveFields?: false | string; query?: string } & Record<string, any> = {}) => {
const returnFields = defaultQueryParam(query, resolveFields);
if (returnFields) {
return getItems({ listKey, context, returnFields, ...args });
return exec(args, returnFields, context);
} else {
return dbAPI.findMany(args);
return dbAPIVersionOfAPI(args);
}
},
};
};
const gqlNames = context.gqlNames(listKey);
return {
findOne: f('query', gqlNames.itemQueryName, dbAPI.findOne),
findMany: f('query', gqlNames.listQueryName, dbAPI.findMany),
async count(args = {}) {
const { first, skip = 0, where = {} } = args;
const { listQueryMetaName, whereInputName } = context.gqlNames(listKey);
const query = `query ($first: Int, $skip: Int! = 0, $where: ${whereInputName}! = {}) { ${listQueryMetaName}(first: $first, skip: $skip, where: $where) { count } }`;
const response = await context.graphql.run({ query, variables: { first, skip, where } });
return response[listQueryMetaName].count;
},
createOne({ query, resolveFields, ...args }) {
const returnFields = defaultQueryParam(query, resolveFields);
if (returnFields) {
const { data } = args;
return createItem({ listKey, context, returnFields, item: data });
} else {
return dbAPI.createOne(args);
}
},
createMany({ query, resolveFields, ...args }) {
const returnFields = defaultQueryParam(query, resolveFields);
if (returnFields) {
const { data } = args;
return createItems({ listKey, context, returnFields, items: data });
} else {
return dbAPI.createMany(args);
}
},
updateOne({ query, resolveFields, ...args }) {
const returnFields = defaultQueryParam(query, resolveFields);
if (returnFields) {
const { id, data } = args;
return updateItem({ listKey, context, returnFields, item: { id, data } });
} else {
return dbAPI.updateOne(args);
}
},
updateMany({ query, resolveFields, ...args }) {
const returnFields = defaultQueryParam(query, resolveFields);
if (returnFields) {
const { data } = args;
return updateItems({ listKey, context, returnFields, items: data });
} else {
return dbAPI.updateMany(args);
}
},
deleteOne({ query, resolveFields, ...args }) {
const returnFields = defaultQueryParam(query, resolveFields);
if (returnFields) {
const { id } = args;
return deleteItem({ listKey, context, returnFields, itemId: id });
} else {
return dbAPI.deleteOne(args);
}
},
deleteMany({ query, resolveFields, ...args }) {
const returnFields = defaultQueryParam(query, resolveFields);
if (returnFields) {
const { ids } = args;
return deleteItems({ listKey, context, returnFields, items: ids });
} else {
return dbAPI.deleteMany(args);
}
},
createOne: f('mutation', gqlNames.createMutationName, dbAPI.createOne),
createMany: f('mutation', gqlNames.createManyMutationName, dbAPI.createMany),
updateOne: f('mutation', gqlNames.updateMutationName, dbAPI.updateOne),
updateMany: f('mutation', gqlNames.updateManyMutationName, dbAPI.updateMany),
deleteOne: f('mutation', gqlNames.deleteMutationName, dbAPI.deleteOne),
deleteMany: f('mutation', gqlNames.deleteManyMutationName, dbAPI.deleteMany),
};
}
Loading

1 comment on commit 7716315

@vercel
Copy link

@vercel vercel bot commented on 7716315 Jul 21, 2021

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.