diff --git a/CHANGELOG.md b/CHANGELOG.md index 70a6de5e..5f796e77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support for generating GraphQL descriptions from CDS doc comments of services, entities, and elements + ### Changed - Bump `@graphiql/plugin-explorer` version to `^1` diff --git a/lib/schema/query.js b/lib/schema/query.js index 14d54ce9..a3a96f89 100644 --- a/lib/schema/query.js +++ b/lib/schema/query.js @@ -11,8 +11,10 @@ module.exports = cache => { for (const key in services) { const service = services[key] const serviceName = gqlName(service.name) - const resolve = resolvers[serviceName] - fields[serviceName] = { type: _serviceToObjectType(service), resolve } + fields[serviceName] = { + type: _serviceToObjectType(service), + resolve: resolvers[serviceName] + } } return new GraphQLObjectType({ name: 'Query', fields }) @@ -33,7 +35,12 @@ module.exports = cache => { fields[gqlName(key)] = { type, args } } - return new GraphQLObjectType({ name: gqlName(service.name), fields }) + return new GraphQLObjectType({ + name: gqlName(service.name), + // REVISIT: Passed services currently don't directly contain doc property + description: service.model.definitions[service.name].doc, + fields + }) } return { generateQueryObjectType } diff --git a/lib/schema/types/object.js b/lib/schema/types/object.js index 288490df..55ded662 100644 --- a/lib/schema/types/object.js +++ b/lib/schema/types/object.js @@ -29,7 +29,11 @@ module.exports = cache => { if (cachedEntityObjectType) return cachedEntityObjectType const fields = {} - const newEntityObjectType = new GraphQLObjectType({ name: entityName, fields: () => fields }) + const newEntityObjectType = new GraphQLObjectType({ + name: entityName, + description: entity.doc, + fields: () => fields + }) cache.set(entityName, newEntityObjectType) for (const name in entity.elements) { @@ -38,7 +42,7 @@ module.exports = cache => { // REVISIT: requires differentiation for support of configurable schema flavors const type = element.is2many ? _elementToObjectConnectionType(element) : _elementToObjectType(element) if (type) { - const field = { type } + const field = { type, description: element.doc } if (element.is2many) field.args = argsGenerator(cache).generateArgumentsForType(element) fields[gqlName(name)] = field } diff --git a/test/resources/bookshop-graphql/db/schema.cds b/test/resources/bookshop-graphql/db/schema.cds index 8748f4ed..b4c93d36 100644 --- a/test/resources/bookshop-graphql/db/schema.cds +++ b/test/resources/bookshop-graphql/db/schema.cds @@ -8,6 +8,7 @@ extend bookshop.Books with { on chapters.book = $self; } +/** A Chapter of a Book */ entity Chapters : managed { key book : Association to bookshop.Books; key number : Integer; diff --git a/test/resources/bookshop-graphql/package.json b/test/resources/bookshop-graphql/package.json index 83fbf746..34b9d665 100644 --- a/test/resources/bookshop-graphql/package.json +++ b/test/resources/bookshop-graphql/package.json @@ -13,6 +13,9 @@ "auth": { "kind": "mocked" } + }, + "cdsc": { + "docs": true } } } diff --git a/test/resources/bookshop-graphql/srv/admin-service.cds b/test/resources/bookshop-graphql/srv/admin-service.cds index acc9fa8d..55abebca 100644 --- a/test/resources/bookshop-graphql/srv/admin-service.cds +++ b/test/resources/bookshop-graphql/srv/admin-service.cds @@ -2,6 +2,7 @@ using {sap.capire.graphql} from '../db/schema'; using {AdminService} from '../../bookshop/srv/admin-service'; @graphql +/** Service used by administrators to manage Books and Authors */ extend service AdminService with { entity Chapters as projection on graphql.Chapters; } diff --git a/test/schemas/bookshop-graphql.gql b/test/schemas/bookshop-graphql.gql index 88287fb1..5679217a 100644 --- a/test/schemas/bookshop-graphql.gql +++ b/test/schemas/bookshop-graphql.gql @@ -1,3 +1,4 @@ +"""Service used by administrators to manage Books and Authors""" type AdminService { Authors(filter: [AdminService_Authors_filter], orderBy: [AdminService_Authors_orderBy], skip: Int, top: Int): AdminService_Authors_connection Books(filter: [AdminService_Books_filter], orderBy: [AdminService_Books_orderBy], skip: Int, top: Int): AdminService_Books_connection @@ -9,14 +10,23 @@ type AdminService { Genres_texts(filter: [AdminService_Genres_texts_filter], orderBy: [AdminService_Genres_texts_orderBy], skip: Int, top: Int): AdminService_Genres_texts_connection } +""" +Aspect to capture changes by user and name + +See https://cap.cloud.sap/docs/cds/common#aspect-managed +""" type AdminService_Authors { ID: Int books(filter: [AdminService_Books_filter], orderBy: [AdminService_Books_orderBy], skip: Int, top: Int): AdminService_Books_connection createdAt: Timestamp + + """Canonical user ID""" createdBy: String dateOfBirth: Date dateOfDeath: Date modifiedAt: Timestamp + + """Canonical user ID""" modifiedBy: String name: String placeOfBirth: String @@ -87,13 +97,26 @@ input AdminService_Authors_orderBy { placeOfDeath: SortDirection } +""" +Aspect to capture changes by user and name + +See https://cap.cloud.sap/docs/cds/common#aspect-managed +""" type AdminService_Books { ID: Int author: AdminService_Authors author_ID: Int chapters(filter: [AdminService_Chapters_filter], orderBy: [AdminService_Chapters_orderBy], skip: Int, top: Int): AdminService_Chapters_connection createdAt: Timestamp + + """Canonical user ID""" createdBy: String + + """ + Type for an association to Currencies + + See https://cap.cloud.sap/docs/cds/common#type-currency + """ currency: AdminService_Currencies currency_code: String descr: String @@ -101,6 +124,8 @@ type AdminService_Books { genre_ID: Int image: Binary modifiedAt: Timestamp + + """Canonical user ID""" modifiedBy: String price: Decimal stock: Int @@ -195,6 +220,8 @@ input AdminService_Books_orderBy { type AdminService_Books_texts { ID: Int descr: String + + """Type for a language code""" locale: String title: String } @@ -236,12 +263,17 @@ input AdminService_Books_texts_orderBy { title: SortDirection } +"""A Chapter of a Book""" type AdminService_Chapters { book: AdminService_Books book_ID: Int createdAt: Timestamp + + """Canonical user ID""" createdBy: String modifiedAt: Timestamp + + """Canonical user ID""" modifiedBy: String number: Int title: String @@ -297,6 +329,11 @@ input AdminService_Chapters_orderBy { title: SortDirection } +""" +Code list for currencies + +See https://cap.cloud.sap/docs/cds/common#entity-currencies +""" type AdminService_Currencies { code: String descr: String @@ -353,6 +390,8 @@ input AdminService_Currencies_orderBy { type AdminService_Currencies_texts { code: String descr: String + + """Type for a language code""" locale: String name: String } @@ -394,6 +433,7 @@ input AdminService_Currencies_texts_orderBy { name: SortDirection } +"""Hierarchically organized Code List for Genres""" type AdminService_Genres { ID: Int children(filter: [AdminService_Genres_filter], orderBy: [AdminService_Genres_orderBy], skip: Int, top: Int): AdminService_Genres_connection @@ -451,6 +491,8 @@ input AdminService_Genres_orderBy { type AdminService_Genres_texts { ID: Int descr: String + + """Type for a language code""" locale: String name: String } @@ -524,11 +566,18 @@ type CatalogService { ListOfBooks(filter: [CatalogService_ListOfBooks_filter], orderBy: [CatalogService_ListOfBooks_orderBy], skip: Int, top: Int): CatalogService_ListOfBooks_connection } +"""For display in details pages""" type CatalogService_Books { ID: Int author: String chapters(filter: [CatalogService_Chapters_filter], orderBy: [CatalogService_Chapters_orderBy], skip: Int, top: Int): CatalogService_Chapters_connection createdAt: Timestamp + + """ + Type for an association to Currencies + + See https://cap.cloud.sap/docs/cds/common#type-currency + """ currency: CatalogService_Currencies currency_code: String descr: String @@ -619,6 +668,8 @@ input CatalogService_Books_orderBy { type CatalogService_Books_texts { ID: Int descr: String + + """Type for a language code""" locale: String title: String } @@ -660,12 +711,17 @@ input CatalogService_Books_texts_orderBy { title: SortDirection } +"""A Chapter of a Book""" type CatalogService_Chapters { book: CatalogService_Books book_ID: Int createdAt: Timestamp + + """Canonical user ID""" createdBy: String modifiedAt: Timestamp + + """Canonical user ID""" modifiedBy: String number: Int title: String @@ -721,6 +777,11 @@ input CatalogService_Chapters_orderBy { title: SortDirection } +""" +Code list for currencies + +See https://cap.cloud.sap/docs/cds/common#entity-currencies +""" type CatalogService_Currencies { code: String descr: String @@ -777,6 +838,8 @@ input CatalogService_Currencies_orderBy { type CatalogService_Currencies_texts { code: String descr: String + + """Type for a language code""" locale: String name: String } @@ -818,6 +881,7 @@ input CatalogService_Currencies_texts_orderBy { name: SortDirection } +"""Hierarchically organized Code List for Genres""" type CatalogService_Genres { ID: Int children(filter: [CatalogService_Genres_filter], orderBy: [CatalogService_Genres_orderBy], skip: Int, top: Int): CatalogService_Genres_connection @@ -875,6 +939,8 @@ input CatalogService_Genres_orderBy { type CatalogService_Genres_texts { ID: Int descr: String + + """Type for a language code""" locale: String name: String } @@ -916,11 +982,18 @@ input CatalogService_Genres_texts_orderBy { name: SortDirection } +"""For displaying lists of Books""" type CatalogService_ListOfBooks { ID: Int author: String chapters(filter: [CatalogService_Chapters_filter], orderBy: [CatalogService_Chapters_orderBy], skip: Int, top: Int): CatalogService_Chapters_connection createdAt: Timestamp + + """ + Type for an association to Currencies + + See https://cap.cloud.sap/docs/cds/common#type-currency + """ currency: CatalogService_Currencies currency_code: String genre: CatalogService_Genres diff --git a/test/scripts/generate-schemas.js b/test/scripts/generate-schemas.js index 57e08a1d..67a2146a 100644 --- a/test/scripts/generate-schemas.js +++ b/test/scripts/generate-schemas.js @@ -12,7 +12,7 @@ const { models } = require('../resources') fs.mkdirSync(SCHEMAS_DIR) for (const model of models) { console.log(`Generating GraphQL schema "${model.name}.gql"`) - const csn = await cds.load(model.files) + const csn = await cds.load(model.files, { docs: true }) const graphQLSchema = cds.compile(csn).to.gql({ sort: true }) const schemaPath = path.join(SCHEMAS_DIR, `${model.name}.gql`) const schemaPathDir = path.parse(schemaPath).dir diff --git a/test/tests/schema.test.js b/test/tests/schema.test.js index 0090bc51..569fb7db 100644 --- a/test/tests/schema.test.js +++ b/test/tests/schema.test.js @@ -16,7 +16,7 @@ describe('graphql - schema generation', () => { describe('generated schema should match saved schema', () => { models.forEach(model => { it('should process model ' + model.name, async () => { - const csn = await cds.load(model.files) + const csn = await cds.load(model.files, { docs: true }) const generatedSchemaObject = cds.compile(csn).to.graphql({ as: 'obj', sort: true }) const schemaValidationErrors = validateSchema(generatedSchemaObject) expect(schemaValidationErrors.length).toEqual(0)