Skip to content

Commit

Permalink
Ensure relationship input types respect graphql.isEnabled (#6408)
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie authored Aug 26, 2021
1 parent 2324fa0 commit 0a98c26
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 92 deletions.
17 changes: 7 additions & 10 deletions packages/keystone/src/fields/types/relationship/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,14 @@ export const relationship =
return resolve(value);
},
},
create: {
arg: graphql.arg({
type: listTypes.relateTo.many.create,
}),
create: listTypes.relateTo.many.create && {
arg: graphql.arg({ type: listTypes.relateTo.many.create }),
async resolve(value, context, resolve) {
return resolve(value);
},
},
update: {
arg: graphql.arg({
type: listTypes.relateTo.many.update,
}),
update: listTypes.relateTo.many.update && {
arg: graphql.arg({ type: listTypes.relateTo.many.update }),
async resolve(value, context, resolve) {
return resolve(value);
},
Expand Down Expand Up @@ -211,13 +207,14 @@ export const relationship =
return resolve(value);
},
},
create: {
create: listTypes.relateTo.one.create && {
arg: graphql.arg({ type: listTypes.relateTo.one.create }),
async resolve(value, context, resolve) {
return resolve(value);
},
},
update: {

update: listTypes.relateTo.one.update && {
arg: graphql.arg({ type: listTypes.relateTo.one.update }),
async resolve(value, context, resolve) {
return resolve(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { isRejected, isFulfilled } from '../utils';
import { NestedMutationState } from './create-update';

type _CreateValueType = Exclude<
graphql.InferValueFromArg<graphql.Arg<TypesForList['relateTo']['many']['create']>>,
graphql.InferValueFromArg<
graphql.Arg<Exclude<TypesForList['relateTo']['many']['create'], undefined>>
>,
null | undefined
>;

type _UpdateValueType = Exclude<
graphql.InferValueFromArg<graphql.Arg<TypesForList['relateTo']['many']['update']>>,
graphql.InferValueFromArg<
graphql.Arg<Exclude<TypesForList['relateTo']['many']['update'], undefined>>
>,
null | undefined
>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { InitialisedList } from '../types-for-lists';
import { NestedMutationState } from './create-update';

type _CreateValueType = Exclude<
graphql.InferValueFromArg<graphql.Arg<TypesForList['relateTo']['one']['create']>>,
graphql.InferValueFromArg<
graphql.Arg<Exclude<TypesForList['relateTo']['one']['create'], undefined>>
>,
null | undefined
>;
type _UpdateValueType = Exclude<
graphql.InferValueFromArg<
graphql.Arg<graphql.NonNullType<TypesForList['relateTo']['one']['update']>>
graphql.Arg<graphql.NonNullType<Exclude<TypesForList['relateTo']['one']['update'], undefined>>>
>,
null | undefined
>;
Expand Down
105 changes: 53 additions & 52 deletions packages/keystone/src/lib/core/types-for-lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,61 +257,62 @@ export function initialiseLists(
skip: graphql.arg({ type: graphql.nonNull(graphql.Int), defaultValue: 0 }),
};

const relateToManyForCreate = graphql.inputObject({
name: names.relateToManyForCreateInputName,
fields: () => {
const list = lists[listKey];
return {
...(list.access.create !== false && {
create: graphql.arg({ type: graphql.list(graphql.nonNull(create)) }),
}),
connect: graphql.arg({ type: graphql.list(graphql.nonNull(uniqueWhere)) }),
};
},
});

const relateToManyForUpdate = graphql.inputObject({
name: names.relateToManyForUpdateInputName,
fields: () => {
const list = lists[listKey];
return {
disconnect: graphql.arg({ type: graphql.list(graphql.nonNull(uniqueWhere)) }),
set: graphql.arg({ type: graphql.list(graphql.nonNull(uniqueWhere)) }),
...(list.access.create !== false && {
create: graphql.arg({ type: graphql.list(graphql.nonNull(create)) }),
}),
connect: graphql.arg({ type: graphql.list(graphql.nonNull(uniqueWhere)) }),
};
},
});
const _isEnabled = isEnabled[listKey];
let relateToManyForCreate, relateToManyForUpdate, relateToOneForCreate, relateToOneForUpdate;
if (_isEnabled.type) {
relateToManyForCreate = graphql.inputObject({
name: names.relateToManyForCreateInputName,
fields: () => {
return {
// Create via a relationship is only supported if this list allows create
...(_isEnabled.create && {
create: graphql.arg({ type: graphql.list(graphql.nonNull(create)) }),
}),
connect: graphql.arg({ type: graphql.list(graphql.nonNull(uniqueWhere)) }),
};
},
});

const relateToOneForCreate = graphql.inputObject({
name: names.relateToOneForCreateInputName,
fields: () => {
const list = lists[listKey];
return {
...(list.access.create !== false && {
create: graphql.arg({ type: create }),
}),
connect: graphql.arg({ type: uniqueWhere }),
};
},
});
relateToManyForUpdate = graphql.inputObject({
name: names.relateToManyForUpdateInputName,
fields: () => {
return {
// The order of these fields reflects the order in which they are applied
// in the mutation.
disconnect: graphql.arg({ type: graphql.list(graphql.nonNull(uniqueWhere)) }),
set: graphql.arg({ type: graphql.list(graphql.nonNull(uniqueWhere)) }),
// Create via a relationship is only supported if this list allows create
...(_isEnabled.create && {
create: graphql.arg({ type: graphql.list(graphql.nonNull(create)) }),
}),
connect: graphql.arg({ type: graphql.list(graphql.nonNull(uniqueWhere)) }),
};
},
});

const relateToOneForUpdate = graphql.inputObject({
name: names.relateToOneForUpdateInputName,
fields: () => {
const list = lists[listKey];
return {
...(list.access.create !== false && {
create: graphql.arg({ type: create }),
}),
connect: graphql.arg({ type: uniqueWhere }),
disconnect: graphql.arg({ type: graphql.Boolean }),
};
},
});
relateToOneForCreate = graphql.inputObject({
name: names.relateToOneForCreateInputName,
fields: () => {
return {
// Create via a relationship is only supported if this list allows create
...(_isEnabled.create && { create: graphql.arg({ type: create }) }),
connect: graphql.arg({ type: uniqueWhere }),
};
},
});

relateToOneForUpdate = graphql.inputObject({
name: names.relateToOneForUpdateInputName,
fields: () => {
return {
// Create via a relationship is only supported if this list allows create
...(_isEnabled.create && { create: graphql.arg({ type: create }) }),
connect: graphql.arg({ type: uniqueWhere }),
disconnect: graphql.arg({ type: graphql.Boolean }),
};
},
});
}
listInfos[listKey] = {
types: {
output,
Expand Down
8 changes: 4 additions & 4 deletions packages/keystone/src/types/next-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,23 +399,23 @@ export type TypesForList = {
some: graphql.Arg<AnyInputObj>;
none: graphql.Arg<AnyInputObj>;
}>;
create: graphql.InputObjectType<{
create?: graphql.InputObjectType<{
connect: graphql.Arg<graphql.ListType<graphql.NonNullType<TypesForList['uniqueWhere']>>>;
create?: graphql.Arg<graphql.ListType<graphql.NonNullType<TypesForList['create']>>>;
}>;
update: graphql.InputObjectType<{
update?: graphql.InputObjectType<{
disconnect: graphql.Arg<graphql.ListType<graphql.NonNullType<TypesForList['uniqueWhere']>>>;
set: graphql.Arg<graphql.ListType<graphql.NonNullType<TypesForList['uniqueWhere']>>>;
connect: graphql.Arg<graphql.ListType<graphql.NonNullType<TypesForList['uniqueWhere']>>>;
create?: graphql.Arg<graphql.ListType<graphql.NonNullType<TypesForList['create']>>>;
}>;
};
one: {
create: graphql.InputObjectType<{
create?: graphql.InputObjectType<{
create?: graphql.Arg<TypesForList['create']>;
connect: graphql.Arg<TypesForList['uniqueWhere']>;
}>;
update: graphql.InputObjectType<{
update?: graphql.InputObjectType<{
create?: graphql.Arg<TypesForList['create']>;
connect: graphql.Arg<TypesForList['uniqueWhere']>;
disconnect: graphql.Arg<typeof graphql.Boolean>;
Expand Down
14 changes: 13 additions & 1 deletion tests/api-tests/access-control/schema-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { text } from '@keystone-next/keystone/fields';
import { relationship, text } from '@keystone-next/keystone/fields';
import { createSchema, list } from '@keystone-next/keystone';
import { statelessSessions } from '@keystone-next/keystone/session';
import { apiTestConfig } from '../utils';
Expand Down Expand Up @@ -116,6 +116,11 @@ const createFieldStatic = (isEnabled: FieldEnabled) => ({
[getFieldName(isEnabled)]: text({ graphql: { isEnabled } }),
});

const createRelatedFields = (isEnabled: ListEnabled) => ({
[`${getListPrefix(isEnabled)}one`]: relationship({ ref: getListName(isEnabled), many: false }),
[`${getListPrefix(isEnabled)}many`]: relationship({ ref: getListName(isEnabled), many: true }),
});

const lists = createSchema({});

listEnabledVariations.forEach(isEnabled => {
Expand All @@ -128,6 +133,13 @@ listEnabledVariations.forEach(isEnabled => {
});
});

lists.RelatedToAll = list({
fields: Object.assign(
{},
...listEnabledVariations.map(variation => createRelatedFields(variation))
),
});

const config = apiTestConfig({
lists,
session: statelessSessions({ secret: COOKIE_SECRET }),
Expand Down
82 changes: 65 additions & 17 deletions tests/api-tests/access-control/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,28 @@ describe(`Schema`, () => {
let queries: string[],
mutations: string[],
types: string[],
typesByName: Record<string, any>,
fieldTypes: Record<
string,
{ name: string; fields: Record<string, any>; inputFields: Record<string, any> }
>;
let __schema: {
types: { name: string; fields: { name: string }[]; inputFields: { name: string }[] }[];
queryType: { fields: { name: string }[] };
mutationType: { fields: { name: string }[] };
};
beforeAll(async () => {
testEnv = await setupTestEnv({ config });
context = testEnv.testArgs.context;

await testEnv.connect();

const data = await context.graphql.run({ query: introspectionQuery });
const __schema: {
types: { name: string; fields: { name: string }[]; inputFields: { name: string }[] }[];
queryType: { fields: { name: string }[] };
mutationType: { fields: { name: string }[] };
} = data.__schema;
__schema = data.__schema;
queries = __schema.queryType.fields.map(({ name }) => name);
mutations = __schema.mutationType.fields.map(({ name }) => name);
types = __schema.types.map(({ name }) => name);
typesByName = Object.fromEntries(__schema.types.map(t => [t.name, t]));
fieldTypes = Object.fromEntries(
__schema.types.map(type => [
type.name,
Expand All @@ -78,22 +81,67 @@ describe(`Schema`, () => {
test(JSON.stringify(isEnabled === undefined ? 'undefined' : isEnabled), async () => {
const name = getListName(isEnabled);
const gqlNames = getGqlNames({ listKey: name, pluralGraphQLName: `${name}s` });
// The type is used in all the queries and mutations as a return type
if (isEnabled !== false) {
expect(types).toContain(gqlNames.outputTypeName);
} else {
// The type is used in all the queries and mutations as a return type.
if (isEnabled === false) {
expect(types).not.toContain(gqlNames.outputTypeName);
} else {
expect(types).toContain(gqlNames.outputTypeName);
}
if (
isEnabled === undefined ||
isEnabled === true ||
(isEnabled !== false && (isEnabled?.query || isEnabled?.update || isEnabled?.delete))
) {
// Filter types are also available for update/delete/create (thanks
// to nested mutations)

// The whereUnique input type is used in queries and mutations, and
// also in the relateTo input types.
if (isEnabled === false) {
expect(types).not.toContain(gqlNames.whereUniqueInputName);
} else {
expect(types).toContain(gqlNames.whereUniqueInputName);
}

// The relateTo types do not exist if the list has been completely disabled
if (isEnabled === false) {
expect(types).not.toContain(gqlNames.relateToManyForCreateInputName);
expect(types).not.toContain(gqlNames.relateToOneForCreateInputName);
expect(types).not.toContain(gqlNames.relateToManyForUpdateInputName);
expect(types).not.toContain(gqlNames.relateToOneForUpdateInputName);
} else {
expect(types).not.toContain(gqlNames.whereUniqueInputName);
expect(types).toContain(gqlNames.relateToManyForCreateInputName);
expect(types).toContain(gqlNames.relateToOneForCreateInputName);
expect(types).toContain(gqlNames.relateToManyForUpdateInputName);
expect(types).toContain(gqlNames.relateToOneForUpdateInputName);

const createFromMany = typesByName[
gqlNames.relateToManyForCreateInputName
].inputFields.map(({ name }: { name: string }) => name);
const createFromOne = typesByName[gqlNames.relateToOneForCreateInputName].inputFields.map(
({ name }: { name: string }) => name
);
const updateFromMany = typesByName[
gqlNames.relateToManyForUpdateInputName
].inputFields.map(({ name }: { name: string }) => name);
const updateFromOne = typesByName[gqlNames.relateToOneForUpdateInputName].inputFields.map(
({ name }: { name: string }) => name
);

expect(createFromMany).not.toContain('unusedPlaceholder');

if (isEnabled === true || isEnabled === undefined || isEnabled.create) {
expect(createFromMany).toContain('create');
expect(createFromOne).toContain('create');
expect(updateFromMany).toContain('create');
expect(updateFromOne).toContain('create');
} else {
expect(createFromMany).not.toContain('create');
expect(createFromOne).not.toContain('create');
expect(updateFromMany).not.toContain('create');
expect(updateFromOne).not.toContain('create');
}
// The connect/disconnect/set operations are always supported.
expect(createFromMany).toContain('connect');
expect(createFromOne).toContain('connect');
expect(updateFromMany).toContain('connect');
expect(updateFromOne).toContain('connect');
expect(updateFromMany).toContain('disconnect');
expect(updateFromOne).toContain('disconnect');
expect(updateFromMany).toContain('set');
}

// Queries are only accessible when reading
Expand Down
Loading

0 comments on commit 0a98c26

Please sign in to comment.