diff --git a/packages/keystone/src/fields/types/relationship/index.ts b/packages/keystone/src/fields/types/relationship/index.ts index eee326685df..ab74ff7ca5d 100644 --- a/packages/keystone/src/fields/types/relationship/index.ts +++ b/packages/keystone/src/fields/types/relationship/index.ts @@ -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); }, @@ -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); diff --git a/packages/keystone/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts b/packages/keystone/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts index 1c842cdec5c..a7335401054 100644 --- a/packages/keystone/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts +++ b/packages/keystone/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts @@ -5,12 +5,16 @@ import { isRejected, isFulfilled } from '../utils'; import { NestedMutationState } from './create-update'; type _CreateValueType = Exclude< - graphql.InferValueFromArg>, + graphql.InferValueFromArg< + graphql.Arg> + >, null | undefined >; type _UpdateValueType = Exclude< - graphql.InferValueFromArg>, + graphql.InferValueFromArg< + graphql.Arg> + >, null | undefined >; diff --git a/packages/keystone/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts b/packages/keystone/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts index 501f7ef6a17..b3d88c703ca 100644 --- a/packages/keystone/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts +++ b/packages/keystone/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts @@ -4,12 +4,14 @@ import { InitialisedList } from '../types-for-lists'; import { NestedMutationState } from './create-update'; type _CreateValueType = Exclude< - graphql.InferValueFromArg>, + graphql.InferValueFromArg< + graphql.Arg> + >, null | undefined >; type _UpdateValueType = Exclude< graphql.InferValueFromArg< - graphql.Arg> + graphql.Arg>> >, null | undefined >; diff --git a/packages/keystone/src/lib/core/types-for-lists.ts b/packages/keystone/src/lib/core/types-for-lists.ts index 869d7161fe7..3a30286f52a 100644 --- a/packages/keystone/src/lib/core/types-for-lists.ts +++ b/packages/keystone/src/lib/core/types-for-lists.ts @@ -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, diff --git a/packages/keystone/src/types/next-fields.ts b/packages/keystone/src/types/next-fields.ts index b362bb1f0cb..1ac5bd3d156 100644 --- a/packages/keystone/src/types/next-fields.ts +++ b/packages/keystone/src/types/next-fields.ts @@ -399,11 +399,11 @@ export type TypesForList = { some: graphql.Arg; none: graphql.Arg; }>; - create: graphql.InputObjectType<{ + create?: graphql.InputObjectType<{ connect: graphql.Arg>>; create?: graphql.Arg>>; }>; - update: graphql.InputObjectType<{ + update?: graphql.InputObjectType<{ disconnect: graphql.Arg>>; set: graphql.Arg>>; connect: graphql.Arg>>; @@ -411,11 +411,11 @@ export type TypesForList = { }>; }; one: { - create: graphql.InputObjectType<{ + create?: graphql.InputObjectType<{ create?: graphql.Arg; connect: graphql.Arg; }>; - update: graphql.InputObjectType<{ + update?: graphql.InputObjectType<{ create?: graphql.Arg; connect: graphql.Arg; disconnect: graphql.Arg; diff --git a/tests/api-tests/access-control/schema-utils.ts b/tests/api-tests/access-control/schema-utils.ts index 6a0f1589be3..c877f6350dd 100644 --- a/tests/api-tests/access-control/schema-utils.ts +++ b/tests/api-tests/access-control/schema-utils.ts @@ -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'; @@ -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 => { @@ -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 }), diff --git a/tests/api-tests/access-control/schema.test.ts b/tests/api-tests/access-control/schema.test.ts index 44b893416da..d189d1280c9 100644 --- a/tests/api-tests/access-control/schema.test.ts +++ b/tests/api-tests/access-control/schema.test.ts @@ -39,10 +39,16 @@ describe(`Schema`, () => { let queries: string[], mutations: string[], types: string[], + typesByName: Record, fieldTypes: Record< string, { name: string; fields: Record; inputFields: Record } >; + 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; @@ -50,14 +56,11 @@ describe(`Schema`, () => { 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, @@ -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 diff --git a/tests/api-tests/relationships/nested-mutations/create-singular.test.ts b/tests/api-tests/relationships/nested-mutations/create-singular.test.ts index a4b48502e62..5c937e70d1f 100644 --- a/tests/api-tests/relationships/nested-mutations/create-singular.test.ts +++ b/tests/api-tests/relationships/nested-mutations/create-singular.test.ts @@ -38,7 +38,7 @@ const runner = setupTestRunner({ fields: { name: text(), }, - access: { read: false }, + graphql: { isEnabled: { query: false } }, }), EventToGroupNoReadHard: list({ @@ -66,7 +66,7 @@ const runner = setupTestRunner({ fields: { name: text({ graphql: { isEnabled: { filter: true } } }), }, - access: { create: false }, + graphql: { isEnabled: { create: false } }, }), EventToGroupNoCreateHard: list({ @@ -94,7 +94,7 @@ const runner = setupTestRunner({ fields: { name: text(), }, - access: { update: false }, + graphql: { isEnabled: { update: false } }, }), EventToGroupNoUpdateHard: list({ @@ -164,7 +164,7 @@ describe('no access control', () => { describe('with access control', () => { [ { name: 'GroupNoRead', allowed: true, func: 'read: () => false' }, - { name: 'GroupNoReadHard', allowed: true, func: 'read: false' }, + { name: 'GroupNoReadHard', allowed: true, func: 'query: false' }, { name: 'GroupNoCreate', allowed: false, func: 'create: () => false' }, { name: 'GroupNoCreateHard', allowed: false, func: 'create: false' }, { name: 'GroupNoUpdate', allowed: true, func: 'update: () => false' },