Skip to content

Commit

Permalink
Add sudo extension for extendGraphqlSchema (#8298)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Cousens <dcousens@users.noreply.github.com>
  • Loading branch information
dcousens and dcousens committed Feb 13, 2023
1 parent ead1241 commit c14fa5c
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 121 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-papaya-straws.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/auth': patch
---

Fixes `isFilterable: false` throwing an error for identity fields
5 changes: 5 additions & 0 deletions .changeset/six-papayas-bob.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/core': minor
---

Adds a `sudo` GraphQL extension for the `GraphQLSchema` passed to `extendGraphqlSchema`; enabling developers to determine if they are extending the sudo GraphQL schema
1 change: 1 addition & 0 deletions examples/extend-graphql-schema-graphql-ts/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Post {
enum PostStatusType {
draft
published
banned
}

scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6")
Expand Down
18 changes: 18 additions & 0 deletions examples/extend-graphql-schema-graphql-ts/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const lists: Lists = {
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
{ label: 'Banned', value: 'banned' },
],
}),
content: text(),
Expand Down Expand Up @@ -63,6 +64,7 @@ export const extendGraphqlSchema = graphql.extend(base => {
}),
},
});

return {
mutation: {
publishPost: graphql.field({
Expand All @@ -81,6 +83,22 @@ export const extendGraphqlSchema = graphql.extend(base => {
});
},
}),

// only add this mutation for a sudo Context (this is not usable from the API)
...(base.schema.extensions.sudo
? {
banPost: graphql.field({
type: base.object('Post'),
args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) },
resolve(source, { id }, context: Context) {
return context.db.Post.updateOne({
where: { id },
data: { status: 'banned' },
});
},
}),
}
: {}),
},
query: {
recentPosts: graphql.field({
Expand Down
15 changes: 10 additions & 5 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function createAuth<ListTypeInfo extends BaseListTypeInfo>({
*
* Must be added to the extendGraphqlSchema config. Can be composed.
*/
const extendGraphqlSchema = getSchemaExtension({
const authExtendGraphqlSchema = getSchemaExtension({
identityField,
listKey,
secretField,
Expand Down Expand Up @@ -253,6 +253,10 @@ export function createAuth<ListTypeInfo extends BaseListTypeInfo>({
return session !== undefined;
}

function defaultExtendGraphqlSchema<T>(schema: T) {
return schema;
}

/**
* withAuth
*
Expand Down Expand Up @@ -292,8 +296,9 @@ export function createAuth<ListTypeInfo extends BaseListTypeInfo>({
if (!keystoneConfig.session) throw new TypeError('Missing .session configuration');
const session = withItemData(keystoneConfig.session);

const existingExtendGraphQLSchema = keystoneConfig.extendGraphqlSchema;
const { extendGraphqlSchema = defaultExtendGraphqlSchema } = keystoneConfig;
const listConfig = keystoneConfig.lists[listKey];

return {
...keystoneConfig,
ui,
Expand All @@ -302,9 +307,9 @@ export function createAuth<ListTypeInfo extends BaseListTypeInfo>({
...keystoneConfig.lists,
[listKey]: { ...listConfig, fields: { ...listConfig.fields, ...fields } },
},
extendGraphqlSchema: existingExtendGraphQLSchema
? schema => existingExtendGraphQLSchema(extendGraphqlSchema(schema))
: extendGraphqlSchema,
extendGraphqlSchema: schema => {
return extendGraphqlSchema(authExtendGraphqlSchema(schema));
},
};
};

Expand Down
6 changes: 4 additions & 2 deletions packages/auth/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExtendGraphqlSchema, getGqlNames } from '@keystone-6/core/types';
import { getGqlNames } from '@keystone-6/core/types';

import {
assertObjectType,
Expand Down Expand Up @@ -58,13 +58,14 @@ export const getSchemaExtension = ({
passwordResetLink?: AuthTokenTypeConfig;
magicAuthLink?: AuthTokenTypeConfig;
sessionData: string;
}): ExtendGraphqlSchema =>
}) =>
graphql.extend(base => {
const uniqueWhereInputType = assertInputObjectType(
base.schema.getType(`${listKey}WhereUniqueInput`)
);
const identityFieldOnUniqueWhere = uniqueWhereInputType.getFields()[identityField];
if (
base.schema.extensions.sudo &&
identityFieldOnUniqueWhere?.type !== GraphQLString &&
identityFieldOnUniqueWhere?.type !== GraphQLID
) {
Expand All @@ -75,6 +76,7 @@ export const getSchemaExtension = ({
`to the field at ${listKey}.${identityField}`
);
}

const baseSchema = getBaseAuthSchema({
identityField,
listKey,
Expand Down
87 changes: 0 additions & 87 deletions packages/core/src/lib/core/graphql-schema.ts

This file was deleted.

143 changes: 118 additions & 25 deletions packages/core/src/lib/createGraphQLSchema.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,134 @@
import { GraphQLNamedType, GraphQLSchema } from 'graphql';

import type { KeystoneConfig } from '../types';
import { KeystoneMeta } from '../admin-ui/system/adminMetaSchema';
import { graphql } from '../types/schema';
import { AdminMetaRootVal } from '../admin-ui/system/createAdminMeta';
import { InitialisedList } from './core/types-for-lists';
import { getGraphQLSchema } from './core/graphql-schema';

export function createGraphQLSchema(
config: KeystoneConfig,
import { getMutationsForList } from './core/mutations';
import { getQueriesForList } from './core/queries';

function getGraphQLSchema(
lists: Record<string, InitialisedList>,
adminMeta: AdminMetaRootVal
extraFields: {
mutation: Record<string, graphql.Field<unknown, any, graphql.OutputType, string>>;
query: Record<string, graphql.Field<unknown, any, graphql.OutputType, string>>;
},
sudo: boolean
) {
// Start with the core keystone graphQL schema
let graphQLSchema = getGraphQLSchema(lists, {
mutation: config.session
? {
endSession: graphql.field({
type: graphql.nonNull(graphql.Boolean),
async resolve(rootVal, args, context) {
if (context.sessionStrategy) {
await context.sessionStrategy.end({ context });
}
return true;
},
}),
}
: {},
query: {
keystone: graphql.field({
type: graphql.nonNull(KeystoneMeta),
resolve: () => ({ adminMeta }),
const query = graphql.object()({
name: 'Query',
fields: Object.assign(
{},
...Object.values(lists).map(list => getQueriesForList(list)),
extraFields.query
),
});

const updateManyByList: Record<string, graphql.InputObjectType<any>> = {};

const mutation = graphql.object()({
name: 'Mutation',
fields: Object.assign(
{},
...Object.values(lists).map(list => {
const { mutations, updateManyInput } = getMutationsForList(list);
updateManyByList[list.listKey] = updateManyInput;
return mutations;
}),
extraFields.mutation
),
});

return new GraphQLSchema({
query: query.graphQLType,
mutation: mutation.graphQLType,
// not about behaviour, only ordering
types: [...collectTypes(lists, updateManyByList), mutation.graphQLType],
extensions: {
sudo,
},
});
}

function collectTypes(
lists: Record<string, InitialisedList>,
updateManyByList: Record<string, graphql.InputObjectType<any>>
) {
const collectedTypes: GraphQLNamedType[] = [];
for (const list of Object.values(lists)) {
const { isEnabled } = list.graphql;
if (!isEnabled.type) continue;
// adding all of these types explicitly isn't strictly necessary but we do it to create a certain order in the schema
collectedTypes.push(list.types.output.graphQLType);
if (isEnabled.query || isEnabled.update || isEnabled.delete) {
collectedTypes.push(list.types.uniqueWhere.graphQLType);
}
if (isEnabled.query) {
for (const field of Object.values(list.fields)) {
if (
isEnabled.query &&
field.graphql.isEnabled.read &&
field.unreferencedConcreteInterfaceImplementations
) {
// this _IS_ actually necessary since they aren't implicitly referenced by other types, unlike the types above
collectedTypes.push(
...field.unreferencedConcreteInterfaceImplementations.map(x => x.graphQLType)
);
}
}
collectedTypes.push(list.types.where.graphQLType);
collectedTypes.push(list.types.orderBy.graphQLType);
}
if (isEnabled.update) {
collectedTypes.push(list.types.update.graphQLType);
collectedTypes.push(updateManyByList[list.listKey].graphQLType);
}
if (isEnabled.create) {
collectedTypes.push(list.types.create.graphQLType);
}
}
// this is not necessary, just about ordering
collectedTypes.push(graphql.JSON.graphQLType);
return collectedTypes;
}

export function createGraphQLSchema(
config: KeystoneConfig,
lists: Record<string, InitialisedList>,
adminMeta: AdminMetaRootVal,
sudo: boolean
) {
const graphQLSchema = getGraphQLSchema(
lists,
{
mutation: config.session
? {
endSession: graphql.field({
type: graphql.nonNull(graphql.Boolean),
async resolve(rootVal, args, context) {
if (context.sessionStrategy) {
await context.sessionStrategy.end({ context });
}
return true;
},
}),
}
: {},
query: {
keystone: graphql.field({
type: graphql.nonNull(KeystoneMeta),
resolve: () => ({ adminMeta }),
}),
},
},
sudo
);

// Merge in the user defined graphQL API
// merge in the user defined graphQL API
if (config.extendGraphqlSchema) {
graphQLSchema = config.extendGraphqlSchema(graphQLSchema);
return config.extendGraphqlSchema(graphQLSchema);
}

return graphQLSchema;
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/lib/createSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,16 @@ function getSudoGraphQLSchema(config: KeystoneConfig) {
})
),
};

const lists = initialiseLists(transformedConfig);
const adminMeta = createAdminMeta(transformedConfig, lists);
return createGraphQLSchema(transformedConfig, lists, adminMeta);
return createGraphQLSchema(transformedConfig, lists, adminMeta, true);
}

export function createSystem(config: KeystoneConfig) {
const lists = initialiseLists(config);
const adminMeta = createAdminMeta(config, lists);
const graphQLSchema = createGraphQLSchema(config, lists, adminMeta);
const graphQLSchema = createGraphQLSchema(config, lists, adminMeta, false);
const sudoGraphQLSchema = getSudoGraphQLSchema(config);

return {
Expand Down

1 comment on commit c14fa5c

@vercel
Copy link

@vercel vercel bot commented on c14fa5c Feb 13, 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.