diff --git a/CHANGELOG.md b/CHANGELOG.md index 824578d3..61001a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved consistency of handling results of different types returned by custom handlers in CRUD resolvers: + Wrap only objects (i.e. not primitive types or arrays) returned by custom handlers in arrays in create, read, and update resolvers + Delete mutations return the length of an array that is returned by a `DELETE` custom handler or 1 if a single object is returned +- Don't generate fields for key elements in update input objects ### Fixed diff --git a/lib/schema/args/input.js b/lib/schema/args/input.js index 54ac9385..63d08a28 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -4,7 +4,8 @@ const { shouldElementBeIgnored } = require('../util') const { cdsToGraphQLScalarType } = require('../types/scalar') module.exports = cache => { - const entityToInputObjectType = (entity, suffix) => { + const entityToInputObjectType = (entity, isUpdate) => { + const suffix = isUpdate ? '_U' : '_C' const entityName = gqlName(entity.name) + suffix const cachedEntityInputObjectType = cache.get(entityName) @@ -16,19 +17,26 @@ module.exports = cache => { for (const name in entity.elements) { const element = entity.elements[name] - const type = _elementToInputObjectType(element, suffix) + const type = _elementToInputObjectType(element, isUpdate) if (type) fields[gqlName(name)] = { type } } + // fields is empty if update input object is generated for an entity that only contains key elements + if (Object.keys(fields).length === 0) return + return newEntityInputObjectType } - const _elementToInputObjectType = (element, suffix) => { + const _elementToInputObjectType = (element, isUpdate) => { if (shouldElementBeIgnored(element)) return + // No keys in update input object + if (isUpdate && element.key) return + const gqlScalarType = cdsToGraphQLScalarType(element) if (element.isAssociation || element.isComposition) { - const type = gqlScalarType || entityToInputObjectType(element._target, suffix) + // Input objects in deep updates overwrite previous entries with new entries and therefore always act as create input objects + const type = gqlScalarType || entityToInputObjectType(element._target, false) return element.is2one ? type : new GraphQLList(type) } else if (gqlScalarType) { return gqlScalarType diff --git a/lib/schema/mutation.js b/lib/schema/mutation.js index f62f2aa3..b6e12f56 100644 --- a/lib/schema/mutation.js +++ b/lib/schema/mutation.js @@ -16,9 +16,12 @@ module.exports = cache => { const serviceName = gqlName(service.name) const resolve = resolvers[serviceName] - fields[serviceName] = { type: _serviceToObjectType(service), resolve } + const type = _serviceToObjectType(service) + if (type) fields[serviceName] = { type, resolve } } + if (Object.keys(fields).length === 0) return + return new GraphQLObjectType({ name: 'Mutation', fields }) } @@ -28,40 +31,67 @@ module.exports = cache => { for (const key in service.entities) { const entity = service.entities[key] const entityName = gqlName(key) - - fields[entityName] = { type: _entityToObjectType(entity) } + const type = _entityToObjectType(entity) + if (type) fields[entityName] = { type } } + if (Object.keys(fields).length === 0) return + return new GraphQLObjectType({ name: gqlName(service.name) + '_input', fields }) } const _entityToObjectType = entity => { + // Filter out undefined fields + const fields = Object.fromEntries( + Object.entries({ + create: _create(entity), + update: _update(entity), + delete: _delete(entity) + }).filter(([_, v]) => v) + ) + + if (Object.keys(fields).length === 0) return + + return new GraphQLObjectType({ name: gqlName(entity.name) + '_input', fields }) + } + + const _create = entity => { const entityObjectType = objectGenerator(cache).entityToObjectType(entity) - const createInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, '_C') - const updateInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, '_U') - const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) + const createInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, false) + if (!createInputObjectType) return - const createArgs = { + const args = { [ARGS.input]: { type: new GraphQLNonNull(new GraphQLList(createInputObjectType)) } } - const updateArgs = { + return { type: new GraphQLList(entityObjectType), args } + } + + const _update = entity => { + const entityObjectType = objectGenerator(cache).entityToObjectType(entity) + + const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) + const updateInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, true) + // filterInputObjectType is undefined if the entity only contains elements that are associations or compositions + // updateInputObjectType is undefined if it is generated for an entity that only contains key elements + if (!filterInputObjectType || !updateInputObjectType) return + + const args = { + [ARGS.filter]: { type: filterInputObjectType }, [ARGS.input]: { type: new GraphQLNonNull(updateInputObjectType) } } - const deleteArgs = {} + return { type: new GraphQLList(entityObjectType), args } + } - if (filterInputObjectType) { - updateArgs[ARGS.filter] = { type: filterInputObjectType } - deleteArgs[ARGS.filter] = { type: filterInputObjectType } - } + const _delete = entity => { + const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) + // filterInputObjectType is undefined if the entity only contains elements that are associations or compositions + if (!filterInputObjectType) return - const fields = { - create: { type: new GraphQLList(entityObjectType), args: createArgs }, - update: { type: new GraphQLList(entityObjectType), args: updateArgs }, - delete: { type: GraphQLInt, args: deleteArgs } + const args = { + [ARGS.filter]: { type: filterInputObjectType } } - - return new GraphQLObjectType({ name: gqlName(entity.name) + '_input', fields }) + return { type: GraphQLInt, args } } return { generateMutationObjectType } diff --git a/lib/schema/types/object.js b/lib/schema/types/object.js index d7146a71..288490df 100644 --- a/lib/schema/types/object.js +++ b/lib/schema/types/object.js @@ -38,10 +38,9 @@ module.exports = cache => { // REVISIT: requires differentiation for support of configurable schema flavors const type = element.is2many ? _elementToObjectConnectionType(element) : _elementToObjectType(element) if (type) { - fields[gqlName(name)] = { - type, - ...(element.is2many && { args: argsGenerator(cache).generateArgumentsForType(element) }) - } + const field = { type } + if (element.is2many) field.args = argsGenerator(cache).generateArgumentsForType(element) + fields[gqlName(name)] = field } } diff --git a/test/schemas/bookshop-graphql.gql b/test/schemas/bookshop-graphql.gql index 65a0576f..91a49c22 100644 --- a/test/schemas/bookshop-graphql.gql +++ b/test/schemas/bookshop-graphql.gql @@ -83,8 +83,7 @@ input AdminService_Authors_C { } input AdminService_Authors_U { - ID: Int - books: [AdminService_Books_U] + books: [AdminService_Books_C] createdAt: Timestamp createdBy: String dateOfBirth: Date @@ -193,23 +192,22 @@ input AdminService_Books_C { } input AdminService_Books_U { - ID: Int - author: AdminService_Authors_U + author: AdminService_Authors_C author_ID: Int - chapters: [AdminService_Chapters_U] + chapters: [AdminService_Chapters_C] createdAt: Timestamp createdBy: String - currency: AdminService_Currencies_U + currency: AdminService_Currencies_C currency_code: String descr: String - genre: AdminService_Genres_U + genre: AdminService_Genres_C genre_ID: Int image: Binary modifiedAt: Timestamp modifiedBy: String price: Decimal stock: Int - texts: [AdminService_Books_texts_U] + texts: [AdminService_Books_texts_C] title: String } @@ -278,9 +276,7 @@ input AdminService_Books_texts_C { } input AdminService_Books_texts_U { - ID: Int descr: String - locale: String title: String } @@ -339,13 +335,10 @@ input AdminService_Chapters_C { } input AdminService_Chapters_U { - book: AdminService_Books_U - book_ID: Int createdAt: Timestamp createdBy: String modifiedAt: Timestamp modifiedBy: String - number: Int title: String } @@ -411,12 +404,11 @@ input AdminService_Currencies_C { } input AdminService_Currencies_U { - code: String descr: String minorUnit: Int16 name: String symbol: String - texts: [AdminService_Currencies_texts_U] + texts: [AdminService_Currencies_texts_C] } type AdminService_Currencies_connection { @@ -468,9 +460,7 @@ input AdminService_Currencies_texts_C { } input AdminService_Currencies_texts_U { - code: String descr: String - locale: String name: String } @@ -537,13 +527,12 @@ input AdminService_Genres_C { } input AdminService_Genres_U { - ID: Int - children: [AdminService_Genres_U] + children: [AdminService_Genres_C] descr: String name: String - parent: AdminService_Genres_U + parent: AdminService_Genres_C parent_ID: Int - texts: [AdminService_Genres_texts_U] + texts: [AdminService_Genres_texts_C] } type AdminService_Genres_connection { @@ -593,9 +582,7 @@ input AdminService_Genres_texts_C { } input AdminService_Genres_texts_U { - ID: Int descr: String - locale: String name: String } @@ -750,20 +737,19 @@ input CatalogService_Books_C { } input CatalogService_Books_U { - ID: Int author: String - chapters: [CatalogService_Chapters_U] + chapters: [CatalogService_Chapters_C] createdAt: Timestamp - currency: CatalogService_Currencies_U + currency: CatalogService_Currencies_C currency_code: String descr: String - genre: CatalogService_Genres_U + genre: CatalogService_Genres_C genre_ID: Int image: Binary modifiedAt: Timestamp price: Decimal stock: Int - texts: [CatalogService_Books_texts_U] + texts: [CatalogService_Books_texts_C] title: String } @@ -828,9 +814,7 @@ input CatalogService_Books_texts_C { } input CatalogService_Books_texts_U { - ID: Int descr: String - locale: String title: String } @@ -889,13 +873,10 @@ input CatalogService_Chapters_C { } input CatalogService_Chapters_U { - book: CatalogService_Books_U - book_ID: Int createdAt: Timestamp createdBy: String modifiedAt: Timestamp modifiedBy: String - number: Int title: String } @@ -961,12 +942,11 @@ input CatalogService_Currencies_C { } input CatalogService_Currencies_U { - code: String descr: String minorUnit: Int16 name: String symbol: String - texts: [CatalogService_Currencies_texts_U] + texts: [CatalogService_Currencies_texts_C] } type CatalogService_Currencies_connection { @@ -1018,9 +998,7 @@ input CatalogService_Currencies_texts_C { } input CatalogService_Currencies_texts_U { - code: String descr: String - locale: String name: String } @@ -1087,13 +1065,12 @@ input CatalogService_Genres_C { } input CatalogService_Genres_U { - ID: Int - children: [CatalogService_Genres_U] + children: [CatalogService_Genres_C] descr: String name: String - parent: CatalogService_Genres_U + parent: CatalogService_Genres_C parent_ID: Int - texts: [CatalogService_Genres_texts_U] + texts: [CatalogService_Genres_texts_C] } type CatalogService_Genres_connection { @@ -1143,9 +1120,7 @@ input CatalogService_Genres_texts_C { } input CatalogService_Genres_texts_U { - ID: Int descr: String - locale: String name: String } @@ -1226,19 +1201,18 @@ input CatalogService_ListOfBooks_C { } input CatalogService_ListOfBooks_U { - ID: Int author: String - chapters: [CatalogService_Chapters_U] + chapters: [CatalogService_Chapters_C] createdAt: Timestamp - currency: CatalogService_Currencies_U + currency: CatalogService_Currencies_C currency_code: String - genre: CatalogService_Genres_U + genre: CatalogService_Genres_C genre_ID: Int image: Binary modifiedAt: Timestamp price: Decimal stock: Int - texts: [CatalogService_Books_texts_U] + texts: [CatalogService_Books_texts_C] title: String } diff --git a/test/schemas/edge-cases/field-named-localized.gql b/test/schemas/edge-cases/field-named-localized.gql index 02bf9257..83cc5fc1 100644 --- a/test/schemas/edge-cases/field-named-localized.gql +++ b/test/schemas/edge-cases/field-named-localized.gql @@ -29,8 +29,7 @@ input FieldNamedLocalizedService_Root_C { } input FieldNamedLocalizedService_Root_U { - ID: Int - localized: [FieldNamedLocalizedService_localized_U] + localized: [FieldNamedLocalizedService_localized_C] } type FieldNamedLocalizedService_Root_connection { @@ -79,9 +78,8 @@ input FieldNamedLocalizedService_localized_C { } input FieldNamedLocalizedService_localized_U { - ID: Int localized: String - root: FieldNamedLocalizedService_Root_U + root: FieldNamedLocalizedService_Root_C root_ID: Int } diff --git a/test/schemas/model-structure/composition-of-aspect.gql b/test/schemas/model-structure/composition-of-aspect.gql index 9d0bd5b8..74efed0f 100644 --- a/test/schemas/model-structure/composition-of-aspect.gql +++ b/test/schemas/model-structure/composition-of-aspect.gql @@ -30,9 +30,8 @@ input CompositionOfAspectService_Books_C { } input CompositionOfAspectService_Books_U { - chapters: [CompositionOfAspectService_Books_chapters_U] - id: ID - reviews: [CompositionOfAspectService_Books_reviews_U] + chapters: [CompositionOfAspectService_Books_chapters_C] + reviews: [CompositionOfAspectService_Books_reviews_C] } type CompositionOfAspectService_Books_chapters { @@ -43,10 +42,6 @@ input CompositionOfAspectService_Books_chapters_C { id: ID } -input CompositionOfAspectService_Books_chapters_U { - id: ID -} - type CompositionOfAspectService_Books_chapters_connection { nodes: [CompositionOfAspectService_Books_chapters] totalCount: Int @@ -63,10 +58,6 @@ type CompositionOfAspectService_Books_chapters_input { delete( filter: [CompositionOfAspectService_Books_chapters_filter] ): Int - update( - filter: [CompositionOfAspectService_Books_chapters_filter] - input: CompositionOfAspectService_Books_chapters_U! - ): [CompositionOfAspectService_Books_chapters] } input CompositionOfAspectService_Books_chapters_orderBy { @@ -107,10 +98,6 @@ input CompositionOfAspectService_Books_reviews_C { id: ID } -input CompositionOfAspectService_Books_reviews_U { - id: ID -} - type CompositionOfAspectService_Books_reviews_connection { nodes: [CompositionOfAspectService_Books_reviews] totalCount: Int @@ -127,10 +114,6 @@ type CompositionOfAspectService_Books_reviews_input { delete( filter: [CompositionOfAspectService_Books_reviews_filter] ): Int - update( - filter: [CompositionOfAspectService_Books_reviews_filter] - input: CompositionOfAspectService_Books_reviews_U! - ): [CompositionOfAspectService_Books_reviews] } input CompositionOfAspectService_Books_reviews_orderBy { diff --git a/test/schemas/types.gql b/test/schemas/types.gql index 9dbe02f6..ce4f718c 100644 --- a/test/schemas/types.gql +++ b/test/schemas/types.gql @@ -235,7 +235,6 @@ input TypesService_MyEntity_U { myTime: Time myTimestamp: Timestamp myUInt8: UInt8 - myUUID: ID } type TypesService_MyEntity_connection { diff --git a/test/tests/queries/variables.test.js b/test/tests/queries/variables.test.js index 46b89b48..e016c104 100644 --- a/test/tests/queries/variables.test.js +++ b/test/tests/queries/variables.test.js @@ -515,7 +515,7 @@ describe('graphql - variables', () => { { ID: 251, title: 'The Raven' }, { ID: 252, title: 'Eleonora' }, { ID: 271, title: 'Catweazle' } - ] + ] } } }