From 4ec568e42500a362e2814492f8074de114b33ea4 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 11:53:02 +0200 Subject: [PATCH 01/16] Don't generate keys in update input object --- lib/schema/args/input.js | 14 +++++++++----- lib/schema/mutation.js | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/schema/args/input.js b/lib/schema/args/input.js index 54ac9385..6b3b094a 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -3,8 +3,10 @@ const { gqlName } = require('../../utils') const { shouldElementBeIgnored } = require('../util') const { cdsToGraphQLScalarType } = require('../types/scalar') -module.exports = cache => { - const entityToInputObjectType = (entity, suffix) => { +module.exports = (cache, isUpdate) => { + const suffix = isUpdate ? '_U' : '_C' + + const entityToInputObjectType = (entity) => { const entityName = gqlName(entity.name) + suffix const cachedEntityInputObjectType = cache.get(entityName) @@ -16,19 +18,21 @@ module.exports = cache => { for (const name in entity.elements) { const element = entity.elements[name] - const type = _elementToInputObjectType(element, suffix) + const type = _elementToInputObjectType(element) if (type) fields[gqlName(name)] = { type } } return newEntityInputObjectType } - const _elementToInputObjectType = (element, suffix) => { + const _elementToInputObjectType = (element) => { 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) + const type = gqlScalarType || entityToInputObjectType(element._target) 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..b4c88475 100644 --- a/lib/schema/mutation.js +++ b/lib/schema/mutation.js @@ -38,8 +38,8 @@ module.exports = cache => { const _entityToObjectType = entity => { const entityObjectType = objectGenerator(cache).entityToObjectType(entity) - const createInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, '_C') - const updateInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, '_U') + const createInputObjectType = inputObjectGenerator(cache, false).entityToInputObjectType(entity) + const updateInputObjectType = inputObjectGenerator(cache, true).entityToInputObjectType(entity) const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) const createArgs = { From 376742d53e4a3c70016790faccfac68ec5d20eea Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 11:53:21 +0200 Subject: [PATCH 02/16] Regenerate schemas --- test/schemas/bookshop-graphql.gql | 26 ------------------- .../edge-cases/field-named-localized.gql | 2 -- .../model-structure/composition-of-aspect.gql | 9 ++----- test/schemas/types.gql | 1 - 4 files changed, 2 insertions(+), 36 deletions(-) diff --git a/test/schemas/bookshop-graphql.gql b/test/schemas/bookshop-graphql.gql index a6b4be44..da2c1477 100644 --- a/test/schemas/bookshop-graphql.gql +++ b/test/schemas/bookshop-graphql.gql @@ -83,7 +83,6 @@ input AdminService_Authors_C { } input AdminService_Authors_U { - ID: Int books: [AdminService_Books_U] createdAt: Timestamp createdBy: String @@ -193,7 +192,6 @@ input AdminService_Books_C { } input AdminService_Books_U { - ID: Int author: AdminService_Authors_U author_ID: Int chapters: [AdminService_Chapters_U] @@ -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 } @@ -409,7 +402,6 @@ input AdminService_Currencies_C { } input AdminService_Currencies_U { - code: String descr: String name: String symbol: String @@ -463,9 +455,7 @@ input AdminService_Currencies_texts_C { } input AdminService_Currencies_texts_U { - code: String descr: String - locale: String name: String } @@ -532,7 +522,6 @@ input AdminService_Genres_C { } input AdminService_Genres_U { - ID: Int children: [AdminService_Genres_U] descr: String name: String @@ -588,9 +577,7 @@ input AdminService_Genres_texts_C { } input AdminService_Genres_texts_U { - ID: Int descr: String - locale: String name: String } @@ -745,7 +732,6 @@ input CatalogService_Books_C { } input CatalogService_Books_U { - ID: Int author: String chapters: [CatalogService_Chapters_U] createdAt: Timestamp @@ -823,9 +809,7 @@ input CatalogService_Books_texts_C { } input CatalogService_Books_texts_U { - ID: Int descr: String - locale: String title: String } @@ -884,13 +868,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 } @@ -954,7 +935,6 @@ input CatalogService_Currencies_C { } input CatalogService_Currencies_U { - code: String descr: String name: String symbol: String @@ -1008,9 +988,7 @@ input CatalogService_Currencies_texts_C { } input CatalogService_Currencies_texts_U { - code: String descr: String - locale: String name: String } @@ -1077,7 +1055,6 @@ input CatalogService_Genres_C { } input CatalogService_Genres_U { - ID: Int children: [CatalogService_Genres_U] descr: String name: String @@ -1133,9 +1110,7 @@ input CatalogService_Genres_texts_C { } input CatalogService_Genres_texts_U { - ID: Int descr: String - locale: String name: String } @@ -1216,7 +1191,6 @@ input CatalogService_ListOfBooks_C { } input CatalogService_ListOfBooks_U { - ID: Int author: String chapters: [CatalogService_Chapters_U] createdAt: Timestamp diff --git a/test/schemas/edge-cases/field-named-localized.gql b/test/schemas/edge-cases/field-named-localized.gql index 02bf9257..fd09d6b8 100644 --- a/test/schemas/edge-cases/field-named-localized.gql +++ b/test/schemas/edge-cases/field-named-localized.gql @@ -29,7 +29,6 @@ input FieldNamedLocalizedService_Root_C { } input FieldNamedLocalizedService_Root_U { - ID: Int localized: [FieldNamedLocalizedService_localized_U] } @@ -79,7 +78,6 @@ input FieldNamedLocalizedService_localized_C { } input FieldNamedLocalizedService_localized_U { - ID: Int localized: String root: FieldNamedLocalizedService_Root_U 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..14ef5d6a 100644 --- a/test/schemas/model-structure/composition-of-aspect.gql +++ b/test/schemas/model-structure/composition-of-aspect.gql @@ -31,7 +31,6 @@ input CompositionOfAspectService_Books_C { input CompositionOfAspectService_Books_U { chapters: [CompositionOfAspectService_Books_chapters_U] - id: ID reviews: [CompositionOfAspectService_Books_reviews_U] } @@ -43,9 +42,7 @@ input CompositionOfAspectService_Books_chapters_C { id: ID } -input CompositionOfAspectService_Books_chapters_U { - id: ID -} +input CompositionOfAspectService_Books_chapters_U type CompositionOfAspectService_Books_chapters_connection { nodes: [CompositionOfAspectService_Books_chapters] @@ -107,9 +104,7 @@ input CompositionOfAspectService_Books_reviews_C { id: ID } -input CompositionOfAspectService_Books_reviews_U { - id: ID -} +input CompositionOfAspectService_Books_reviews_U type CompositionOfAspectService_Books_reviews_connection { nodes: [CompositionOfAspectService_Books_reviews] 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 { From 935bdb39d3da7fdbba37b91c5262f5979115d5b2 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 11:53:40 +0200 Subject: [PATCH 03/16] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 824578d3..78a18893 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 key elements as fields for update input objects ### Fixed From f93798b4ca5dbe52c000e3821384579315b02459 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 11:55:51 +0200 Subject: [PATCH 04/16] Add todos --- lib/schema/args/input.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/schema/args/input.js b/lib/schema/args/input.js index 6b3b094a..afade2ba 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -27,11 +27,13 @@ module.exports = (cache, isUpdate) => { const _elementToInputObjectType = (element) => { if (shouldElementBeIgnored(element)) return + // TODO: handle empty update input objects for entities that only contain keys // No keys in update input object if (isUpdate && element.key) return const gqlScalarType = cdsToGraphQLScalarType(element) if (element.isAssociation || element.isComposition) { + // TODO: allow keys to be specified in nested update input objects -> should nested type be _C input object? const type = gqlScalarType || entityToInputObjectType(element._target) return element.is2one ? type : new GraphQLList(type) } else if (gqlScalarType) { From c4515eac79c2f6e73fcbf8fbac93c3656e68695f Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 12:49:02 +0200 Subject: [PATCH 05/16] Move suffix const into function --- lib/schema/args/input.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/schema/args/input.js b/lib/schema/args/input.js index afade2ba..8c799aa5 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -4,9 +4,8 @@ const { shouldElementBeIgnored } = require('../util') const { cdsToGraphQLScalarType } = require('../types/scalar') module.exports = (cache, isUpdate) => { - const suffix = isUpdate ? '_U' : '_C' - const entityToInputObjectType = (entity) => { + const suffix = isUpdate ? '_U' : '_C' const entityName = gqlName(entity.name) + suffix const cachedEntityInputObjectType = cache.get(entityName) From ef045b0bc1d97caa8c1e281e29688d2ffd91ccc6 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 14:53:41 +0200 Subject: [PATCH 06/16] Nested input objects in update are create input objects --- lib/schema/args/input.js | 12 ++--- lib/schema/mutation.js | 4 +- test/schemas/bookshop-graphql.gql | 44 +++++++++---------- .../edge-cases/field-named-localized.gql | 4 +- .../model-structure/composition-of-aspect.gql | 4 +- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/schema/args/input.js b/lib/schema/args/input.js index 8c799aa5..5e250b33 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -3,8 +3,8 @@ const { gqlName } = require('../../utils') const { shouldElementBeIgnored } = require('../util') const { cdsToGraphQLScalarType } = require('../types/scalar') -module.exports = (cache, isUpdate) => { - const entityToInputObjectType = (entity) => { +module.exports = cache => { + const entityToInputObjectType = (entity, isUpdate) => { const suffix = isUpdate ? '_U' : '_C' const entityName = gqlName(entity.name) + suffix @@ -17,14 +17,14 @@ module.exports = (cache, isUpdate) => { for (const name in entity.elements) { const element = entity.elements[name] - const type = _elementToInputObjectType(element) + const type = _elementToInputObjectType(element, isUpdate) if (type) fields[gqlName(name)] = { type } } return newEntityInputObjectType } - const _elementToInputObjectType = (element) => { + const _elementToInputObjectType = (element, isUpdate) => { if (shouldElementBeIgnored(element)) return // TODO: handle empty update input objects for entities that only contain keys // No keys in update input object @@ -32,8 +32,8 @@ module.exports = (cache, isUpdate) => { const gqlScalarType = cdsToGraphQLScalarType(element) if (element.isAssociation || element.isComposition) { - // TODO: allow keys to be specified in nested update input objects -> should nested type be _C input object? - const type = gqlScalarType || entityToInputObjectType(element._target) + // 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 b4c88475..02791aec 100644 --- a/lib/schema/mutation.js +++ b/lib/schema/mutation.js @@ -38,8 +38,8 @@ module.exports = cache => { const _entityToObjectType = entity => { const entityObjectType = objectGenerator(cache).entityToObjectType(entity) - const createInputObjectType = inputObjectGenerator(cache, false).entityToInputObjectType(entity) - const updateInputObjectType = inputObjectGenerator(cache, true).entityToInputObjectType(entity) + const createInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, false) + const updateInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, true) const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) const createArgs = { diff --git a/test/schemas/bookshop-graphql.gql b/test/schemas/bookshop-graphql.gql index da2c1477..83ccdd54 100644 --- a/test/schemas/bookshop-graphql.gql +++ b/test/schemas/bookshop-graphql.gql @@ -83,7 +83,7 @@ input AdminService_Authors_C { } input AdminService_Authors_U { - books: [AdminService_Books_U] + books: [AdminService_Books_C] createdAt: Timestamp createdBy: String dateOfBirth: Date @@ -192,22 +192,22 @@ input AdminService_Books_C { } input AdminService_Books_U { - 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 } @@ -405,7 +405,7 @@ input AdminService_Currencies_U { descr: String name: String symbol: String - texts: [AdminService_Currencies_texts_U] + texts: [AdminService_Currencies_texts_C] } type AdminService_Currencies_connection { @@ -522,12 +522,12 @@ input AdminService_Genres_C { } input AdminService_Genres_U { - 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 { @@ -733,18 +733,18 @@ input CatalogService_Books_C { input CatalogService_Books_U { 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 } @@ -938,7 +938,7 @@ input CatalogService_Currencies_U { descr: String name: String symbol: String - texts: [CatalogService_Currencies_texts_U] + texts: [CatalogService_Currencies_texts_C] } type CatalogService_Currencies_connection { @@ -1055,12 +1055,12 @@ input CatalogService_Genres_C { } input CatalogService_Genres_U { - 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 { @@ -1192,17 +1192,17 @@ input CatalogService_ListOfBooks_C { input CatalogService_ListOfBooks_U { 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 fd09d6b8..83cc5fc1 100644 --- a/test/schemas/edge-cases/field-named-localized.gql +++ b/test/schemas/edge-cases/field-named-localized.gql @@ -29,7 +29,7 @@ input FieldNamedLocalizedService_Root_C { } input FieldNamedLocalizedService_Root_U { - localized: [FieldNamedLocalizedService_localized_U] + localized: [FieldNamedLocalizedService_localized_C] } type FieldNamedLocalizedService_Root_connection { @@ -79,7 +79,7 @@ input FieldNamedLocalizedService_localized_C { input FieldNamedLocalizedService_localized_U { 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 14ef5d6a..2fee8d5a 100644 --- a/test/schemas/model-structure/composition-of-aspect.gql +++ b/test/schemas/model-structure/composition-of-aspect.gql @@ -30,8 +30,8 @@ input CompositionOfAspectService_Books_C { } input CompositionOfAspectService_Books_U { - chapters: [CompositionOfAspectService_Books_chapters_U] - reviews: [CompositionOfAspectService_Books_reviews_U] + chapters: [CompositionOfAspectService_Books_chapters_C] + reviews: [CompositionOfAspectService_Books_reviews_C] } type CompositionOfAspectService_Books_chapters { From 4a43cc2aa70fba419089414f04c0da6d1fffff57 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 15:18:41 +0200 Subject: [PATCH 07/16] Omit generation of update input argument if input object has no fields --- lib/schema/args/input.js | 5 ++++- lib/schema/mutation.js | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/schema/args/input.js b/lib/schema/args/input.js index 5e250b33..63d08a28 100644 --- a/lib/schema/args/input.js +++ b/lib/schema/args/input.js @@ -21,12 +21,15 @@ module.exports = cache => { 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, isUpdate) => { if (shouldElementBeIgnored(element)) return - // TODO: handle empty update input objects for entities that only contain keys + // No keys in update input object if (isUpdate && element.key) return diff --git a/lib/schema/mutation.js b/lib/schema/mutation.js index 02791aec..1820d606 100644 --- a/lib/schema/mutation.js +++ b/lib/schema/mutation.js @@ -45,11 +45,13 @@ module.exports = cache => { const createArgs = { [ARGS.input]: { type: new GraphQLNonNull(new GraphQLList(createInputObjectType)) } } - const updateArgs = { - [ARGS.input]: { type: new GraphQLNonNull(updateInputObjectType) } - } + const updateArgs = {} const deleteArgs = {} + // updateInputObjectType is undefined if it is generated for an entity that only contains key elements + if (updateInputObjectType) updateArgs[ARGS.input] = { type: new GraphQLNonNull(updateInputObjectType) } + + // filterInputObjectType is undefined if the entity only contains elements that are associations or compositions if (filterInputObjectType) { updateArgs[ARGS.filter] = { type: filterInputObjectType } deleteArgs[ARGS.filter] = { type: filterInputObjectType } From 8b92d96fce36636ddfb4f0fb1eedbaba84efeb9a Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 15:42:03 +0200 Subject: [PATCH 08/16] Regenerate schemas --- test/schemas/model-structure/composition-of-aspect.gql | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/schemas/model-structure/composition-of-aspect.gql b/test/schemas/model-structure/composition-of-aspect.gql index 2fee8d5a..63c1c033 100644 --- a/test/schemas/model-structure/composition-of-aspect.gql +++ b/test/schemas/model-structure/composition-of-aspect.gql @@ -42,8 +42,6 @@ input CompositionOfAspectService_Books_chapters_C { id: ID } -input CompositionOfAspectService_Books_chapters_U - type CompositionOfAspectService_Books_chapters_connection { nodes: [CompositionOfAspectService_Books_chapters] totalCount: Int @@ -62,7 +60,6 @@ type CompositionOfAspectService_Books_chapters_input { ): Int update( filter: [CompositionOfAspectService_Books_chapters_filter] - input: CompositionOfAspectService_Books_chapters_U! ): [CompositionOfAspectService_Books_chapters] } @@ -104,8 +101,6 @@ input CompositionOfAspectService_Books_reviews_C { id: ID } -input CompositionOfAspectService_Books_reviews_U - type CompositionOfAspectService_Books_reviews_connection { nodes: [CompositionOfAspectService_Books_reviews] totalCount: Int @@ -124,7 +119,6 @@ type CompositionOfAspectService_Books_reviews_input { ): Int update( filter: [CompositionOfAspectService_Books_reviews_filter] - input: CompositionOfAspectService_Books_reviews_U! ): [CompositionOfAspectService_Books_reviews] } From 7494384b7aa851a5cf350893e7cdc61aeb0836a2 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 15:56:13 +0200 Subject: [PATCH 09/16] Don't generate CUD mutation fields when args are missing --- lib/schema/mutation.js | 25 +++++++++++++------ .../model-structure/composition-of-aspect.gql | 6 ----- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/schema/mutation.js b/lib/schema/mutation.js index 1820d606..448789fa 100644 --- a/lib/schema/mutation.js +++ b/lib/schema/mutation.js @@ -42,14 +42,18 @@ module.exports = cache => { const updateInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, true) const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) - const createArgs = { - [ARGS.input]: { type: new GraphQLNonNull(new GraphQLList(createInputObjectType)) } - } + const createArgs = {} const updateArgs = {} const deleteArgs = {} + if (createInputObjectType) { + createArgs[ARGS.input] = { type: new GraphQLNonNull(new GraphQLList(createInputObjectType)) } + } + // updateInputObjectType is undefined if it is generated for an entity that only contains key elements - if (updateInputObjectType) updateArgs[ARGS.input] = { type: new GraphQLNonNull(updateInputObjectType) } + if (updateInputObjectType) { + updateArgs[ARGS.input] = { type: new GraphQLNonNull(updateInputObjectType) } + } // filterInputObjectType is undefined if the entity only contains elements that are associations or compositions if (filterInputObjectType) { @@ -57,10 +61,15 @@ module.exports = cache => { deleteArgs[ARGS.filter] = { type: filterInputObjectType } } - const fields = { - create: { type: new GraphQLList(entityObjectType), args: createArgs }, - update: { type: new GraphQLList(entityObjectType), args: updateArgs }, - delete: { type: GraphQLInt, args: deleteArgs } + const fields = {} + if (createArgs[ARGS.input]) { + fields.create = { type: new GraphQLList(entityObjectType), args: createArgs } + } + if (updateArgs[ARGS.filter] && updateArgs[ARGS.input]) { + fields.update = { type: new GraphQLList(entityObjectType), args: updateArgs } + } + if (deleteArgs[ARGS.filter]) { + fields.delete = { type: GraphQLInt, args: deleteArgs } } return new GraphQLObjectType({ name: gqlName(entity.name) + '_input', fields }) diff --git a/test/schemas/model-structure/composition-of-aspect.gql b/test/schemas/model-structure/composition-of-aspect.gql index 63c1c033..74efed0f 100644 --- a/test/schemas/model-structure/composition-of-aspect.gql +++ b/test/schemas/model-structure/composition-of-aspect.gql @@ -58,9 +58,6 @@ type CompositionOfAspectService_Books_chapters_input { delete( filter: [CompositionOfAspectService_Books_chapters_filter] ): Int - update( - filter: [CompositionOfAspectService_Books_chapters_filter] - ): [CompositionOfAspectService_Books_chapters] } input CompositionOfAspectService_Books_chapters_orderBy { @@ -117,9 +114,6 @@ type CompositionOfAspectService_Books_reviews_input { delete( filter: [CompositionOfAspectService_Books_reviews_filter] ): Int - update( - filter: [CompositionOfAspectService_Books_reviews_filter] - ): [CompositionOfAspectService_Books_reviews] } input CompositionOfAspectService_Books_reviews_orderBy { From 70775f1486e4c12ca0cdaf9b2f393ff672ce6d9d Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 15:58:29 +0200 Subject: [PATCH 10/16] Improve changelog entry wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78a18893..61001a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +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 key elements as fields for update input objects +- Don't generate fields for key elements in update input objects ### Fixed From 0c21e5735ab719dd9f35b59a76c4f62eb93f214b Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 16:23:50 +0200 Subject: [PATCH 11/16] Improve readability of object creation --- lib/schema/types/object.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 } } From a09a86092ca688e6b5b42c5d0ec3b1ab398f555a Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 16:24:53 +0200 Subject: [PATCH 12/16] Ensure mutation related fields are not generated if subfields are empty --- lib/schema/mutation.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/schema/mutation.js b/lib/schema/mutation.js index 448789fa..e0027c94 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,10 +31,12 @@ 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 }) } @@ -72,6 +77,8 @@ module.exports = cache => { fields.delete = { type: GraphQLInt, args: deleteArgs } } + if (Object.keys(fields).length === 0) return + return new GraphQLObjectType({ name: gqlName(entity.name) + '_input', fields }) } From 7b7b71016a689b4a5697018cbdccbc95b6d544bf Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 17:02:32 +0200 Subject: [PATCH 13/16] Extract creation of CUD fields to separate functions --- lib/schema/mutation.js | 62 ++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/lib/schema/mutation.js b/lib/schema/mutation.js index e0027c94..f92af8cc 100644 --- a/lib/schema/mutation.js +++ b/lib/schema/mutation.js @@ -42,44 +42,52 @@ module.exports = cache => { const _entityToObjectType = entity => { const entityObjectType = objectGenerator(cache).entityToObjectType(entity) - - const createInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, false) - const updateInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, true) const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) - const createArgs = {} - const updateArgs = {} - const deleteArgs = {} + const fields = Object.fromEntries( + Object.entries({ + create: _create(entity, entityObjectType), + update: _update(entity, entityObjectType, filterInputObjectType), + delete: _delete(filterInputObjectType) + }).filter(([k, v]) => v) + ) - if (createInputObjectType) { - createArgs[ARGS.input] = { type: new GraphQLNonNull(new GraphQLList(createInputObjectType)) } - } + if (Object.keys(fields).length === 0) return - // updateInputObjectType is undefined if it is generated for an entity that only contains key elements - if (updateInputObjectType) { - updateArgs[ARGS.input] = { type: new GraphQLNonNull(updateInputObjectType) } + return new GraphQLObjectType({ name: gqlName(entity.name) + '_input', fields }) + } + + const _create = (entity, entityObjectType) => { + const createInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, false) + if (!createInputObjectType) return + + const args = { + [ARGS.input]: { type: new GraphQLNonNull(new GraphQLList(createInputObjectType)) } } + return { type: new GraphQLList(entityObjectType), args } + } + const _update = (entity, entityObjectType, filterInputObjectType) => { + const updateInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, true) + // updateInputObjectType is undefined if it is generated for an entity that only contains key elements // filterInputObjectType is undefined if the entity only contains elements that are associations or compositions - if (filterInputObjectType) { - updateArgs[ARGS.filter] = { type: filterInputObjectType } - deleteArgs[ARGS.filter] = { type: filterInputObjectType } - } + if (!updateInputObjectType || !filterInputObjectType) return - const fields = {} - if (createArgs[ARGS.input]) { - fields.create = { type: new GraphQLList(entityObjectType), args: createArgs } - } - if (updateArgs[ARGS.filter] && updateArgs[ARGS.input]) { - fields.update = { type: new GraphQLList(entityObjectType), args: updateArgs } - } - if (deleteArgs[ARGS.filter]) { - fields.delete = { type: GraphQLInt, args: deleteArgs } + const args = { + [ARGS.filter]: { type: filterInputObjectType }, + [ARGS.input]: { type: new GraphQLNonNull(updateInputObjectType) } } + return { type: new GraphQLList(entityObjectType), args } + } - if (Object.keys(fields).length === 0) return + const _delete = filterInputObjectType => { + // filterInputObjectType is undefined if the entity only contains elements that are associations or compositions + if (!filterInputObjectType) return - return new GraphQLObjectType({ name: gqlName(entity.name) + '_input', fields }) + const args = { + [ARGS.filter]: { type: filterInputObjectType } + } + return { type: GraphQLInt, args } } return { generateMutationObjectType } From c728334601f6eabad6223eb2ecf42ac25dacbb8c Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 17:07:21 +0200 Subject: [PATCH 14/16] Move creation of args input objects into functions since they are cached --- lib/schema/mutation.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/schema/mutation.js b/lib/schema/mutation.js index f92af8cc..145c64df 100644 --- a/lib/schema/mutation.js +++ b/lib/schema/mutation.js @@ -41,15 +41,13 @@ module.exports = cache => { } const _entityToObjectType = entity => { - const entityObjectType = objectGenerator(cache).entityToObjectType(entity) - const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) - + // Filter out undefined fields const fields = Object.fromEntries( Object.entries({ - create: _create(entity, entityObjectType), - update: _update(entity, entityObjectType, filterInputObjectType), - delete: _delete(filterInputObjectType) - }).filter(([k, v]) => v) + create: _create(entity), + update: _update(entity), + delete: _delete(entity) + }).filter(([_, v]) => v) ) if (Object.keys(fields).length === 0) return @@ -57,7 +55,9 @@ module.exports = cache => { return new GraphQLObjectType({ name: gqlName(entity.name) + '_input', fields }) } - const _create = (entity, entityObjectType) => { + const _create = entity => { + const entityObjectType = objectGenerator(cache).entityToObjectType(entity) + const createInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, false) if (!createInputObjectType) return @@ -67,7 +67,10 @@ module.exports = cache => { return { type: new GraphQLList(entityObjectType), args } } - const _update = (entity, entityObjectType, filterInputObjectType) => { + const _update = entity => { + const entityObjectType = objectGenerator(cache).entityToObjectType(entity) + + const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) const updateInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, true) // updateInputObjectType is undefined if it is generated for an entity that only contains key elements // filterInputObjectType is undefined if the entity only contains elements that are associations or compositions @@ -80,7 +83,8 @@ module.exports = cache => { return { type: new GraphQLList(entityObjectType), args } } - const _delete = 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 From b5b924a91e7d02d8e573e0b75639394019d2f6f6 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Thu, 6 Apr 2023 17:10:44 +0200 Subject: [PATCH 15/16] Reorder vars and comments --- lib/schema/mutation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/schema/mutation.js b/lib/schema/mutation.js index 145c64df..b6e12f56 100644 --- a/lib/schema/mutation.js +++ b/lib/schema/mutation.js @@ -72,9 +72,9 @@ module.exports = cache => { const filterInputObjectType = filterGenerator(cache).generateFilterForEntity(entity) const updateInputObjectType = inputObjectGenerator(cache).entityToInputObjectType(entity, true) - // updateInputObjectType is undefined if it is generated for an entity that only contains key elements // filterInputObjectType is undefined if the entity only contains elements that are associations or compositions - if (!updateInputObjectType || !filterInputObjectType) return + // 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 }, From c8e5d0b1a7919975b54ff92381fd5b26b4f2dad4 Mon Sep 17 00:00:00 2001 From: Marcel Schwarz Date: Mon, 17 Apr 2023 15:41:06 +0200 Subject: [PATCH 16/16] Prettier format --- test/tests/queries/variables.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' } - ] + ] } } }