Skip to content

Commit

Permalink
feat: move registration of graphql and gql cds.compile.to targe…
Browse files Browse the repository at this point in the history
…ts from `@sap/cds` (#125)

* Move compile to gql/graphql to module from @sap/cds

* Use `generateSchema4` instead of `SchemaGenerator`

* Remove unused `SchemaGenerator` class

* Extract `cds.compile.to.(gql|graphql)` registration

* Prettier format

* Reword comment

* Simplify generate-schemas script

* Fix module paths

* WIP comment out require cli

* Add option to compile to GraphQLSchema object

* Add compile option to lexicographically sort schema

* Fix registration of gql and graphql getters

* `Object.defineProperty` -> `Object.defineProperties`

* Rename `compile` imports to `cds_compile_to_gql`

* Extract require to lazy function for readability

* Remove default options destructuring from args

* Rename `cli.js` to `api.js`

* Re-add `SchemaGenerator` class for backwards compatibility

* `options.object` -> `options.as: 'obj'` to align with `cds.compile.to.sql`

* Default options to empty object

* Fix `SchemaGenerator.generate()` chaining

* Fix missing import

* Ensure plugin is loaded to extend `compile` in bookshop-graphql project

* Remove `formatSchema` util function

* Make compile format regex case insensitive

* Extract CSN to variable for better readability

* Add changelog entry

* Improve lazy loading and clarify purpose of api module

* Shorten registration and avoid specifying properties twice

* Rename lazy function

* Use `.to.gql` and `.to.graphql` in tests

* Use `cds.compile.to` in enrich test

* Ensure compile target registration only happens once

* Move `cds.compile.to` into register function

* Registration in `GraphQLAdapter` is not necessary since the plugin should always be loaded

* Add comment to explain why all compile targets are lazy loaded

* Comment wording

* Swap require const order

* Clarify revisit comment
  • Loading branch information
schwma authored Oct 11, 2023
1 parent 321ec06 commit ea4cb89
Show file tree
Hide file tree
Showing 10 changed files with 82 additions and 33 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Moved registration of `cds.compile.to.gql` and `cds.compile.to.graphql` targets from `@sap/cds` to `@cap-js/graphql`

### Fixed

### Removed
Expand Down
1 change: 1 addition & 0 deletions cds-plugin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const cds = require('@sap/cds')
require('./lib/api').registerCompileTargets()
const protocols = cds.env.protocols ??= {}
if (!protocols.graphql) protocols.graphql = {
path: "/graphql", impl: "@cap-js/graphql"
Expand Down
23 changes: 23 additions & 0 deletions lib/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const cds = require('@sap/cds')

const TARGETS = ['gql', 'graphql']

function _lazyRegisterCompileTargets() {
const value = require('./compile')
// Lazy load all compile targets if any of them is accessed
// For example .to.gql was called -> load .to.gql and .to.graphql
TARGETS.forEach(target => Object.defineProperty(this, target, { value }))
return value
}

// Register gql and graphql as cds.compile.to targets
const registerCompileTargets = () => {
TARGETS.forEach(target =>
Object.defineProperty(cds.compile.to, target, {
get: _lazyRegisterCompileTargets,
configurable: true
})
)
}

module.exports = { registerCompileTargets }
17 changes: 17 additions & 0 deletions lib/compile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const cds = require('@sap/cds')
const { generateSchema4 } = require('./schema')
const { lexicographicSortSchema, printSchema } = require('graphql')

function cds_compile_to_gql(csn, options = {}) {
const m = cds.linked(csn)
const services = Object.fromEntries(m.services.map(s => [s.name, new cds.ApplicationService(s.name, m)]))

let schema = generateSchema4(services)

if (options.sort) schema = lexicographicSortSchema(schema)
if (/^obj|object$/i.test(options.as)) return schema

return printSchema(schema)
}

module.exports = cds_compile_to_gql
19 changes: 11 additions & 8 deletions lib/schema/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
const queryGenerator = require('./query')
const mutationGenerator = require('./mutation')
const { GraphQLSchema, printSchema } = require('graphql')
const { GraphQLSchema } = require('graphql')
const { createRootResolvers, registerAliasFieldResolvers } = require('../resolvers')
const { printSchema } = require('graphql')

// REVISIT: remove class with cds^8
class SchemaGenerator {
generate(services) {
const resolvers = createRootResolvers(services)
const cache = new Map()
const query = queryGenerator(cache).generateQueryObjectType(services, resolvers.Query)
const mutation = mutationGenerator(cache).generateMutationObjectType(services, resolvers.Mutation)
this._schema = new GraphQLSchema({ query, mutation })
registerAliasFieldResolvers(this._schema)
this._schema = generateSchema4(services)
return this
}

Expand All @@ -24,7 +21,13 @@ class SchemaGenerator {
}

function generateSchema4(services) {
return new SchemaGenerator().generate(services).getSchema()
const resolvers = createRootResolvers(services)
const cache = new Map()
const query = queryGenerator(cache).generateQueryObjectType(services, resolvers.Query)
const mutation = mutationGenerator(cache).generateMutationObjectType(services, resolvers.Mutation)
const schema = new GraphQLSchema({ query, mutation })
registerAliasFieldResolvers(schema)
return schema
}

module.exports = { SchemaGenerator, generateSchema4 }
3 changes: 3 additions & 0 deletions test/resources/bookshop-graphql/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"dependencies": {
"@cap-js/graphql": "../../.."
},
"cds": {
"requires": {
"db": {
Expand Down
9 changes: 6 additions & 3 deletions test/scripts/generate-schemas.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
#!/usr/bin/env node
const cds = require('@sap/cds')
// Load @cap-js/graphql plugin to ensure .to.gql and .to.graphql compile targets are registered
require('../../cds-plugin')
const path = require('path')
const fs = require('fs')
const { SCHEMAS_DIR, cdsFilesToGQLSchema, formatSchema } = require('../util')
const { SCHEMAS_DIR } = require('../util')
const { models } = require('../resources')
const { printSchema } = require('graphql')

;(async () => {
fs.rmSync(SCHEMAS_DIR, { recursive: true, force: true })
fs.mkdirSync(SCHEMAS_DIR)
for (const model of models) {
console.log(`Generating GraphQL schema "${model.name}.gql"`)
const graphQLSchema = formatSchema(printSchema(await cdsFilesToGQLSchema(model.files)))
const csn = await cds.load(model.files)
const graphQLSchema = cds.compile(csn).to.gql({ sort: true })
const schemaPath = path.join(SCHEMAS_DIR, `${model.name}.gql`)
const schemaPathDir = path.parse(schemaPath).dir
if (!fs.existsSync(schemaPathDir)) fs.mkdirSync(schemaPathDir)
Expand Down
8 changes: 6 additions & 2 deletions test/tests/enrich.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
describe('graphql - enrich AST with parsed inline literal values', () => {
const cds = require('@sap/cds')
// Load @cap-js/graphql plugin to ensure .to.gql and .to.graphql compile targets are registered
require('../../cds-plugin')
const { gql } = require('../util')
const { parse } = require('graphql')
const enrich = require('../../lib/resolvers/parse/ast/enrich')
const { models } = require('../resources')
const { cdsFilesToGQLSchema, fakeInfoObject } = require('../util')
const { fakeInfoObject } = require('../util')

let bookshopSchema

beforeAll(async () => {
const bookshopModel = models.find(m => m.name === 'bookshop-graphql')
bookshopSchema = await cdsFilesToGQLSchema(bookshopModel.files)
const csn = await cds.load(bookshopModel.files)
bookshopSchema = cds.compile(csn).to.gql({ as: 'object' })
})

test('parsing of literal value as top level argument', async () => {
Expand Down
18 changes: 11 additions & 7 deletions test/tests/schema.test.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
const fs = require('fs')
const path = require('path')
const semver = require('semver')
const cdsVersion = require('@sap/cds').version
const { version: cdsVersion } = require('@sap/cds')
// Load @cap-js/graphql plugin to ensure .to.gql and .to.graphql compile targets are registered
require('../../cds-plugin')

let { models } = require('../resources')
models = models.filter(m => !m.requires_cds || semver.satisfies(cdsVersion, m.requires_cds))
const models = require('../resources').models.filter(
m => !m.requires_cds || semver.satisfies(cdsVersion, m.requires_cds)
)

const { SCHEMAS_DIR, cdsFilesToGQLSchema, formatSchema } = require('../util')
const { SCHEMAS_DIR } = require('../util')
const { printSchema, validateSchema } = require('graphql')

describe('graphql - schema generation', () => {
describe('generated schema should match saved schema', () => {
models.forEach(model => {
it('should process model ' + model.name, async () => {
const generatedSchemaObject = await cdsFilesToGQLSchema(model.files)
const csn = await cds.load(model.files)
const generatedSchemaObject = cds.compile(csn).to.graphql({ as: 'obj', sort: true })
const schemaValidationErrors = validateSchema(generatedSchemaObject)
expect(schemaValidationErrors.length).toEqual(0)

const loadedSchema = formatSchema(fs.readFileSync(path.join(SCHEMAS_DIR, `${model.name}.gql`), 'utf-8'))
const generatedSchema = formatSchema(printSchema(generatedSchemaObject))
const loadedSchema = fs.readFileSync(path.join(SCHEMAS_DIR, `${model.name}.gql`), 'utf-8')
const generatedSchema = printSchema(generatedSchemaObject)
expect(loadedSchema).toEqual(generatedSchema)
})
})
Expand Down
15 changes: 2 additions & 13 deletions test/util/index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
const path = require('path')
const { buildSchema, lexicographicSortSchema, printSchema, Kind } = require('graphql')
const { Kind } = require('graphql')

const SCHEMAS_DIR = path.join(__dirname, '../schemas')

const cdsFilesToGQLSchema = async files => {
const cds = require('@sap/cds/lib')
const { generateSchema4 } = require('../../lib/schema')

const m = cds.linked(await cds.load(files))
const services = Object.fromEntries(m.services.map(s => [s.name, new cds.ApplicationService(s.name, m)]))
return generateSchema4(services)
}

const formatSchema = schemaString => printSchema(lexicographicSortSchema(buildSchema(schemaString)))

/**
* Create a fake/mock object that matches the structure of the info object that is passed to resolver functions by the graphql.js library.
*
Expand Down Expand Up @@ -44,4 +33,4 @@ const fakeInfoObject = (document, schema, parentTypeName, variables) => {
*/
const gql = String.raw

module.exports = { SCHEMAS_DIR, cdsFilesToGQLSchema, formatSchema, fakeInfoObject, gql }
module.exports = { SCHEMAS_DIR, fakeInfoObject, gql }

0 comments on commit ea4cb89

Please sign in to comment.