@cap-js/graphql
Advanced tools
Comparing version 0.9.0 to 0.10.0
@@ -8,2 +8,20 @@ # Changelog | ||
## Version 0.10.0 - 2023-01-30 | ||
### Added | ||
- Support for generating GraphQL descriptions from CDS doc comments of services, entities, and elements | ||
- Support for operator `in` for filtering on lists of values | ||
### Changed | ||
- Bump `@graphiql/plugin-explorer` version to `^1` | ||
- Ignore fields that represent foreign keys in GraphQL schema generation | ||
- When compiling to GraphQL using the CDS API or CLI, only generate GraphQL schemas for services that are annotated with GraphQL protocol annotations | ||
### Fixed | ||
- Name clashes when CDS elements are named `nodes` or `totalCount` | ||
- Parsing of `null` values in lists | ||
## Version 0.9.0 - 2023-11-16 | ||
@@ -10,0 +28,0 @@ |
@@ -5,5 +5,26 @@ const cds = require('@sap/cds') | ||
const _isServiceAnnotatedWithGraphQL = service => { | ||
const { definition } = service | ||
if (definition['@graphql']) return true | ||
const protocol = definition['@protocol'] | ||
if (protocol) { | ||
// @protocol: 'graphql' or @protocol: ['graphql', 'odata'] | ||
const protocols = Array.isArray(protocol) ? protocol : [protocol] | ||
// Normalize objects such as { kind: 'graphql' } to strings | ||
return protocols.map(p => (typeof p === 'object' ? p.kind : p)).some(p => p.match(/graphql/i)) | ||
} | ||
return false | ||
} | ||
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)])) | ||
const services = Object.fromEntries( | ||
m.services | ||
.map(s => [s.name, new cds.ApplicationService(s.name, m)]) | ||
// Only compile services with GraphQL endpoints | ||
.filter(([_, service]) => _isServiceAnnotatedWithGraphQL(service)) | ||
) | ||
@@ -10,0 +31,0 @@ let schema = generateSchema4(services) |
@@ -23,2 +23,6 @@ const CONNECTION_FIELDS = { | ||
const LOGICAL_OPERATORS = { | ||
in: 'in' | ||
} | ||
const STRING_OPERATIONS = { | ||
@@ -30,3 +34,3 @@ startswith: 'startswith', | ||
const OPERATOR_CONJUNCTION_SUPPORT = { | ||
const OPERATOR_LIST_SUPPORT = { | ||
[RELATIONAL_OPERATORS.eq]: false, | ||
@@ -38,2 +42,3 @@ [RELATIONAL_OPERATORS.ne]: true, | ||
[RELATIONAL_OPERATORS.lt]: false, | ||
[LOGICAL_OPERATORS.in]: true, | ||
[STRING_OPERATIONS.startswith]: false, | ||
@@ -48,4 +53,5 @@ [STRING_OPERATIONS.endswith]: false, | ||
RELATIONAL_OPERATORS, | ||
LOGICAL_OPERATORS, | ||
STRING_OPERATIONS, | ||
OPERATOR_CONJUNCTION_SUPPORT | ||
OPERATOR_LIST_SUPPORT | ||
} |
@@ -7,3 +7,2 @@ const cds = require('@sap/cds/lib') | ||
const GraphQLRequest = require('../GraphQLRequest') | ||
const { isPlainObject } = require('../utils') | ||
const formatResult = require('../parse/ast/result') | ||
@@ -19,5 +18,4 @@ | ||
const result = await service.dispatch(new GraphQLRequest({ req, res, query })) | ||
const resultInArray = isPlainObject(result) ? [result] : result | ||
return formatResult(selection, resultInArray, true) | ||
return formatResult(entity, selection, result, false) | ||
} |
@@ -6,3 +6,2 @@ const cds = require('@sap/cds/lib') | ||
const GraphQLRequest = require('../GraphQLRequest') | ||
const { isPlainObject } = require('../utils') | ||
const formatResult = require('../parse/ast/result') | ||
@@ -15,3 +14,3 @@ | ||
let query = SELECT.from(entity) | ||
query.columns(astToColumns(selection.selectionSet.selections)) | ||
query.columns(astToColumns(entity, selection.selectionSet.selections, true)) | ||
@@ -35,5 +34,3 @@ const filter = getArgumentByName(args, ARGS.filter) | ||
const resultInArray = isPlainObject(result) ? [result] : result | ||
return formatResult(selection, resultInArray) | ||
return formatResult(entity, selection, result, true) | ||
} |
@@ -7,3 +7,2 @@ const cds = require('@sap/cds/lib') | ||
const GraphQLRequest = require('../GraphQLRequest') | ||
const { isPlainObject } = require('../utils') | ||
const formatResult = require('../parse/ast/result') | ||
@@ -17,3 +16,3 @@ | ||
const queryBeforeUpdate = SELECT.from(entity) | ||
queryBeforeUpdate.columns(astToColumns(selection.selectionSet.selections)) | ||
queryBeforeUpdate.columns(astToColumns(entity, selection.selectionSet.selections, false)) | ||
@@ -45,5 +44,3 @@ if (filter) queryBeforeUpdate.where(astToWhere(filter)) | ||
const resultInArray = isPlainObject(mergedResults) ? [mergedResults] : mergedResults | ||
return formatResult(selection, resultInArray, true) | ||
return formatResult(entity, selection, mergedResults, false) | ||
} |
@@ -1,91 +0,41 @@ | ||
const { Kind } = require('graphql') | ||
const { visit, Kind } = require('graphql') | ||
const fragmentSpreadSelections = require('./fragment') | ||
const substituteVariable = require('./variable') | ||
const removeMetaFieldsFromSelections = require('./meta') | ||
const parseLiteral = require('./literal') | ||
const _traverseObjectValue = (info, objectValue, _fields) => | ||
objectValue.fields.forEach(field => { | ||
const _field = _fields[field.name.value] | ||
_traverseArgumentOrObjectField(info, field, _field) | ||
}) | ||
module.exports = info => { | ||
const rootTypeName = info.parentType.name | ||
const rootType = info.schema.getType(rootTypeName) | ||
const rootFields = rootType.getFields() | ||
const _traverseListValue = (info, listValue, _fields) => { | ||
for (let i = 0; i < listValue.values.length; i++) { | ||
const value = listValue.values[i] | ||
switch (value.kind) { | ||
case Kind.VARIABLE: | ||
listValue.values[i] = substituteVariable(info, value) | ||
break | ||
case Kind.OBJECT: | ||
_traverseObjectValue(info, value, _fields) | ||
break | ||
const enrichedAST = visit(info.fieldNodes, { | ||
[Kind.SELECTION_SET](node) { | ||
// Substitute fragment spreads with fragment definitions into the AST as if they were inline fields | ||
// Prevents the necessity for special handling of fragments in AST to CQN | ||
// Note: FragmentSpread visitor function cannot be used to replace fragment spreads with fragment definitions | ||
// that contain multiple top level selections, since those must be placed directly into the selection set | ||
node.selections = fragmentSpreadSelections(info, node.selections) | ||
}, | ||
[Kind.FIELD](node) { | ||
// Remove __typename from selections to prevent field from being interpreted as DB column | ||
// Instead let graphql framework determine the type | ||
if (node.name?.value === '__typename') return null | ||
}, | ||
// Literals within the AST have not yet been parsed | ||
[Kind.ARGUMENT]: parseLiteral(rootFields), | ||
// Literals within the AST have not yet been parsed | ||
[Kind.OBJECT_FIELD]: parseLiteral(rootFields), | ||
[Kind.VARIABLE](node) { | ||
// Substitute variable values into the AST as if they were literal values | ||
// Prevents the necessity for special handling of variables in AST to CQN | ||
return substituteVariable(info, node) | ||
}, | ||
[Kind.NULL](node) { | ||
// Convenience value for handling of null values in AST to CQN | ||
node.value = null | ||
} | ||
} | ||
} | ||
const _isScalarKind = kind => kind === Kind.INT || kind === Kind.FLOAT || kind === Kind.STRING || kind === Kind.BOOLEAN | ||
const _traverseArgumentOrObjectField = (info, argumentOrObjectField, _fieldOr_arg) => { | ||
const value = argumentOrObjectField.value | ||
const type = _getTypeFrom_fieldOr_arg(_fieldOr_arg) | ||
if (_isScalarKind(value.kind)) value.value = type.parseLiteral(value) | ||
switch (value.kind) { | ||
case Kind.VARIABLE: | ||
argumentOrObjectField.value = substituteVariable(info, value) | ||
break | ||
case Kind.LIST: | ||
_traverseListValue(info, value, type.getFields?.()) | ||
break | ||
case Kind.OBJECT: | ||
_traverseObjectValue(info, value, type.getFields()) | ||
break | ||
} | ||
// Convenience value for both literal and variable values | ||
if (argumentOrObjectField.value?.kind === Kind.NULL) argumentOrObjectField.value.value = null | ||
} | ||
const _traverseSelectionSet = (info, selectionSet, _fields) => { | ||
selectionSet.selections = fragmentSpreadSelections(info, selectionSet.selections) | ||
selectionSet.selections = removeMetaFieldsFromSelections(selectionSet.selections) | ||
selectionSet.selections.forEach(field => { | ||
const _field = _fields[field.name.value] | ||
_traverseField(info, field, _field) | ||
}) | ||
} | ||
const _getTypeFrom_fieldOr_arg = _field => { | ||
let type = _field.type | ||
while (type.ofType) type = type.ofType | ||
return type | ||
return enrichedAST | ||
} | ||
const _traverseField = (info, field, _field) => { | ||
if (field.selectionSet) { | ||
const type = _getTypeFrom_fieldOr_arg(_field) | ||
_traverseSelectionSet(info, field.selectionSet, type.getFields()) | ||
} | ||
field.arguments.forEach(arg => { | ||
const _arg = _field.args.find(a => a.name === arg.name.value) | ||
_traverseArgumentOrObjectField(info, arg, _arg) | ||
}) | ||
} | ||
const _traverseFieldNodes = (info, fieldNodes, _fields) => | ||
fieldNodes.forEach(fieldNode => { | ||
const _field = _fields[fieldNode.name.value] | ||
_traverseField(info, fieldNode, _field) | ||
}) | ||
module.exports = info => { | ||
const deepClonedFieldNodes = JSON.parse(JSON.stringify(info.fieldNodes)) | ||
const rootTypeName = info.parentType.name | ||
const rootType = info.schema.getType(rootTypeName) | ||
_traverseFieldNodes(info, deepClonedFieldNodes, rootType.getFields()) | ||
return deepClonedFieldNodes | ||
} |
@@ -10,5 +10,5 @@ const { Kind } = require('graphql') | ||
selections.flatMap(selection => | ||
selection.kind === Kind.FRAGMENT_SPREAD ? _substituteFragment(info, selection) : [selection] | ||
selection.kind === Kind.FRAGMENT_SPREAD ? _substituteFragment(info, selection) : selection | ||
) | ||
module.exports = fragmentSpreadSelections |
@@ -6,5 +6,21 @@ const { Kind } = require('graphql') | ||
const _valueToGenericScalarValue = value => ({ | ||
kind: 'GenericScalarValue', | ||
value | ||
const _valueToGraphQLType = value => { | ||
if (typeof value === 'boolean') return Kind.BOOLEAN | ||
if (typeof value === 'number') { | ||
if (Number.isInteger(value)) return Kind.INT | ||
return Kind.FLOAT | ||
} | ||
// Return below means: (typeof value === 'string' || Buffer.isBuffer(value)) | ||
return Kind.STRING | ||
} | ||
const _valueToScalarValue = value => ({ | ||
kind: _valueToGraphQLType(value), | ||
value, | ||
// Variable values have already been parsed | ||
// -> skip parsing in Argument and ObjectField visitor functions | ||
skipParsing: true | ||
}) | ||
@@ -34,21 +50,13 @@ | ||
const _variableToValue = variable => { | ||
if (Array.isArray(variable)) { | ||
return _arrayToListValue(variable) | ||
} | ||
if (Array.isArray(variable)) return _arrayToListValue(variable) | ||
if (isPlainObject(variable)) { | ||
return _objectToObjectValue(variable) | ||
} | ||
if (isPlainObject(variable)) return _objectToObjectValue(variable) | ||
if (variable === null) { | ||
return _nullValue | ||
} | ||
if (variable === null) return _nullValue | ||
if (variable === undefined) { | ||
return undefined | ||
} | ||
if (variable === undefined) return undefined | ||
return _valueToGenericScalarValue(variable) | ||
return _valueToScalarValue(variable) | ||
} | ||
module.exports = variableValue => _variableToValue(variableValue) |
@@ -5,4 +5,4 @@ const { CONNECTION_FIELDS } = require('../../../constants') | ||
const formatResult = (field, result, skipTopLevelConnection) => { | ||
const _formatObject = (selections, object) => { | ||
const formatResult = (entity, field, result, isConnection) => { | ||
const _formatObject = (type, selections, object) => { | ||
const result = {} | ||
@@ -16,4 +16,6 @@ | ||
for (const field of fields) { | ||
const element = type.elements[field.name.value] | ||
const elementType = element._target || element | ||
const responseKey = field.alias?.value || field.name.value | ||
result[responseKey] = _formatByType(field, value) | ||
result[responseKey] = _formatByType(elementType, field, value, element.is2many) | ||
} | ||
@@ -34,9 +36,9 @@ } | ||
const _formatArray = (field, array, skipTopLevelConnection) => { | ||
const _formatArray = (type, field, array, isConnection) => { | ||
const selections = field.selectionSet.selections | ||
const potentiallyNestedSelections = getPotentiallyNestedNodesSelections(selections) | ||
const result = array.map(e => _formatObject(potentiallyNestedSelections, e)) | ||
const potentiallyNestedSelections = getPotentiallyNestedNodesSelections(selections, isConnection) | ||
const result = array.map(e => _formatObject(type, potentiallyNestedSelections, e)) | ||
// REVISIT: requires differentiation for support of configurable schema flavors | ||
if (skipTopLevelConnection) return result | ||
if (!isConnection) return result | ||
@@ -49,6 +51,6 @@ return { | ||
const _formatByType = (field, value, skipTopLevelConnection) => { | ||
if (Array.isArray(value)) return _formatArray(field, value, skipTopLevelConnection) | ||
const _formatByType = (type, field, value, isConnection) => { | ||
if (Array.isArray(value)) return _formatArray(type, field, value, isConnection) | ||
if (isPlainObject(value)) return _formatObject(field.selectionSet.selections, value) | ||
if (isPlainObject(value)) return _formatObject(type, field.selectionSet.selections, value) | ||
@@ -58,5 +60,7 @@ return value | ||
return _formatByType(field, result, skipTopLevelConnection) | ||
// TODO: if singleton, don't wrap in array | ||
const resultInArray = isPlainObject(result) ? [result] : result | ||
return _formatByType(entity, field, resultInArray, isConnection) | ||
} | ||
module.exports = formatResult |
@@ -8,9 +8,10 @@ const { getPotentiallyNestedNodesSelections } = require('../util') | ||
const astToColumns = selections => { | ||
const astToColumns = (entity, selections, isConnection) => { | ||
let columns = [] | ||
for (const selection of getPotentiallyNestedNodesSelections(selections)) { | ||
for (const selection of getPotentiallyNestedNodesSelections(selections, isConnection)) { | ||
const args = selection.arguments | ||
const fieldName = selection.name.value | ||
const element = entity.elements[fieldName] | ||
const column = { ref: [fieldName] } | ||
@@ -20,3 +21,3 @@ if (selection.alias) column.as = selection.alias.value | ||
if (selection.selectionSet?.selections) { | ||
const columns = astToColumns(selection.selectionSet.selections) | ||
const columns = astToColumns(element._target, selection.selectionSet.selections, element.is2many) | ||
// columns is empty if only __typename was selected (which was filtered out in the enriched AST) | ||
@@ -23,0 +24,0 @@ column.expand = columns.length > 0 ? columns : ['*'] |
@@ -1,2 +0,2 @@ | ||
const { RELATIONAL_OPERATORS, STRING_OPERATIONS } = require('../../../constants') | ||
const { RELATIONAL_OPERATORS, LOGICAL_OPERATORS, STRING_OPERATIONS } = require('../../../constants') | ||
const { Kind } = require('graphql') | ||
@@ -16,3 +16,4 @@ | ||
[RELATIONAL_OPERATORS.le]: '<=', | ||
[RELATIONAL_OPERATORS.lt]: '<' | ||
[RELATIONAL_OPERATORS.lt]: '<', | ||
[LOGICAL_OPERATORS.in]: 'in' | ||
} | ||
@@ -34,9 +35,15 @@ | ||
const gqlOperator = objectField.name.value | ||
const operand = objectField.value | ||
if (objectField.value.kind === Kind.LIST) { | ||
const _xprs = objectField.value.values.map(value => _to_xpr(ref, gqlOperator, value.value)) | ||
if (gqlOperator === LOGICAL_OPERATORS.in) { | ||
const list = operand.kind === Kind.LIST ? operand.values.map(value => ({ val: value.value })) : [{ val: operand.value }] | ||
return [ref, _gqlOperatorToCdsOperator(gqlOperator), { list }] | ||
} | ||
if (operand.kind === Kind.LIST) { | ||
const _xprs = operand.values.map(value => _to_xpr(ref, gqlOperator, value.value)) | ||
return _joinedXprFrom_xprs(_xprs, 'and') | ||
} | ||
return _to_xpr(ref, gqlOperator, objectField.value.value) | ||
return _to_xpr(ref, gqlOperator, operand.value) | ||
} | ||
@@ -43,0 +50,0 @@ |
@@ -1,3 +0,1 @@ | ||
const { CONNECTION_FIELDS } = require('../../../constants') | ||
const _filterOutDuplicateColumnsSelections = selections => { | ||
@@ -18,7 +16,5 @@ const mergedSelectionsMap = new Map() | ||
const getPotentiallyNestedNodesSelections = selections => { | ||
const isConnection = selections.some(selection => Object.values(CONNECTION_FIELDS).includes(selection.name.value)) | ||
return isConnection ? _filterOutDuplicateColumnsSelections(selections) : selections | ||
} | ||
const getPotentiallyNestedNodesSelections = (selections, isConnection) => | ||
isConnection ? _filterOutDuplicateColumnsSelections(selections) : selections | ||
module.exports = { getPotentiallyNestedNodesSelections } |
@@ -13,3 +13,3 @@ const { | ||
const { cdsToGraphQLScalarType } = require('../types/scalar') | ||
const { RELATIONAL_OPERATORS, STRING_OPERATIONS, OPERATOR_CONJUNCTION_SUPPORT } = require('../../constants') | ||
const { RELATIONAL_OPERATORS, LOGICAL_OPERATORS, STRING_OPERATIONS, OPERATOR_LIST_SUPPORT } = require('../../constants') | ||
const { | ||
@@ -60,2 +60,3 @@ GraphQLBinary, | ||
const _generateScalarFilter = gqlType => { | ||
const numericOperators = Object.values({ ...RELATIONAL_OPERATORS, ...LOGICAL_OPERATORS }) | ||
const filterType = { | ||
@@ -65,18 +66,18 @@ // REVISIT: which filters for binary | ||
[GraphQLBoolean.name]: _generateFilterType(GraphQLBoolean, [RELATIONAL_OPERATORS.eq, RELATIONAL_OPERATORS.ne]), | ||
[GraphQLDate.name]: _generateFilterType(GraphQLDate, Object.values(RELATIONAL_OPERATORS)), | ||
[GraphQLDateTime.name]: _generateFilterType(GraphQLDateTime, Object.values(RELATIONAL_OPERATORS)), | ||
[GraphQLDecimal.name]: _generateFilterType(GraphQLDecimal, Object.values(RELATIONAL_OPERATORS)), | ||
// REVISIT: should 'eq'/'ne' be generated since exact comparisons could be difficult due to floating point errors? | ||
[GraphQLFloat.name]: _generateFilterType(GraphQLFloat, Object.values(RELATIONAL_OPERATORS)), | ||
[GraphQLID.name]: _generateFilterType(GraphQLID, Object.values(RELATIONAL_OPERATORS)), | ||
[GraphQLInt.name]: _generateFilterType(GraphQLInt, Object.values(RELATIONAL_OPERATORS)), | ||
[GraphQLInt16.name]: _generateFilterType(GraphQLInt16, Object.values(RELATIONAL_OPERATORS)), | ||
[GraphQLInt64.name]: _generateFilterType(GraphQLInt64, Object.values(RELATIONAL_OPERATORS)), | ||
[GraphQLDate.name]: _generateFilterType(GraphQLDate, numericOperators), | ||
[GraphQLDateTime.name]: _generateFilterType(GraphQLDateTime, numericOperators), | ||
[GraphQLDecimal.name]: _generateFilterType(GraphQLDecimal, numericOperators), | ||
// REVISIT: should 'eq'/'ne'/'in' be generated since exact comparisons could be difficult due to floating point errors? | ||
[GraphQLFloat.name]: _generateFilterType(GraphQLFloat, numericOperators), | ||
[GraphQLID.name]: _generateFilterType(GraphQLID, numericOperators), | ||
[GraphQLInt.name]: _generateFilterType(GraphQLInt, numericOperators), | ||
[GraphQLInt16.name]: _generateFilterType(GraphQLInt16, numericOperators), | ||
[GraphQLInt64.name]: _generateFilterType(GraphQLInt64, numericOperators), | ||
[GraphQLString.name]: _generateFilterType( | ||
GraphQLString, | ||
Object.values({ ...RELATIONAL_OPERATORS, ...STRING_OPERATIONS }) | ||
Object.values({ ...RELATIONAL_OPERATORS, ...LOGICAL_OPERATORS, ...STRING_OPERATIONS }) | ||
), | ||
[GraphQLTime.name]: _generateFilterType(GraphQLTime, Object.values(RELATIONAL_OPERATORS)), | ||
[GraphQLTimestamp.name]: _generateFilterType(GraphQLTimestamp, Object.values(RELATIONAL_OPERATORS)), | ||
[GraphQLUInt8.name]: _generateFilterType(GraphQLUInt8, Object.values(RELATIONAL_OPERATORS)) | ||
[GraphQLTime.name]: _generateFilterType(GraphQLTime, numericOperators), | ||
[GraphQLTimestamp.name]: _generateFilterType(GraphQLTimestamp, numericOperators), | ||
[GraphQLUInt8.name]: _generateFilterType(GraphQLUInt8, numericOperators) | ||
}[gqlType.name] | ||
@@ -92,6 +93,3 @@ return new GraphQLList(filterType) | ||
const ops = operations.map(op => [ | ||
[op], | ||
{ type: OPERATOR_CONJUNCTION_SUPPORT[op] ? new GraphQLList(gqlType) : gqlType } | ||
]) | ||
const ops = operations.map(op => [[op], { type: OPERATOR_LIST_SUPPORT[op] ? new GraphQLList(gqlType) : gqlType }]) | ||
const fields = Object.fromEntries(ops) | ||
@@ -98,0 +96,0 @@ const newFilterType = new GraphQLInputObjectType({ name: filterName, fields }) |
@@ -14,4 +14,6 @@ const { GraphQLObjectType } = require('graphql') | ||
const serviceName = gqlName(service.name) | ||
const resolve = resolvers[serviceName] | ||
fields[serviceName] = { type: _serviceToObjectType(service), resolve } | ||
fields[serviceName] = { | ||
type: _serviceToObjectType(service), | ||
resolve: resolvers[serviceName] | ||
} | ||
} | ||
@@ -37,3 +39,8 @@ | ||
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 | ||
}) | ||
} | ||
@@ -40,0 +47,0 @@ |
@@ -32,7 +32,12 @@ const { GraphQLScalarType, Kind, GraphQLError } = require('graphql') | ||
const parseLiteral = valueNode => { | ||
if (valueNode.kind !== Kind.STRING) throw getGraphQLValueError(ERROR_NON_STRING_VALUE, valueNode) | ||
const { kind, value } = valueNode | ||
const buffer = Buffer.from(valueNode.value, 'base64') | ||
_validateBase64String(valueNode.value, buffer, valueNode) | ||
if (kind !== Kind.STRING) throw getGraphQLValueError(ERROR_NON_STRING_VALUE, valueNode) | ||
// WORKAROUND: value could have already been parsed to a Buffer, necessary because of manual parsing in enrich AST | ||
if (Buffer.isBuffer(value)) return value | ||
const buffer = Buffer.from(value, 'base64') | ||
_validateBase64String(value, buffer, valueNode) | ||
return buffer | ||
@@ -39,0 +44,0 @@ } |
@@ -10,3 +10,3 @@ const { GraphQLScalarType, Kind } = require('graphql') | ||
// Cut off milliseconds | ||
return date.replace(/\.\d\d\dZ$/, 'Z') | ||
return date.slice(0, 19) + 'Z' | ||
} | ||
@@ -13,0 +13,0 @@ |
@@ -26,3 +26,5 @@ const { GraphQLScalarType, Kind } = require('graphql') | ||
const parseLiteral = valueNode => { | ||
if (valueNode.kind !== Kind.FLOAT && valueNode.kind !== Kind.INT && valueNode.kind !== Kind.STRING) | ||
const { kind, value } = valueNode | ||
if (kind !== Kind.FLOAT && kind !== Kind.INT && kind !== Kind.STRING) | ||
throw getGraphQLValueError(ERROR_NON_NUMERIC_VALUE, valueNode) | ||
@@ -32,3 +34,3 @@ | ||
return valueNode.value | ||
return value | ||
} | ||
@@ -35,0 +37,0 @@ |
@@ -31,5 +31,6 @@ const { GraphQLScalarType, Kind } = require('graphql') | ||
const parseLiteral = valueNode => { | ||
if (valueNode.kind !== Kind.INT && valueNode.kind !== Kind.STRING) | ||
throw getGraphQLValueError(ERROR_NON_INTEGER_VALUE, valueNode) | ||
const { kind } = valueNode | ||
if (kind !== Kind.INT && kind !== Kind.STRING) throw getGraphQLValueError(ERROR_NON_INTEGER_VALUE, valueNode) | ||
const num = _toBigInt(valueNode) | ||
@@ -36,0 +37,0 @@ validateRange(num, MIN_INT64, MAX_INT64, ERROR_NON_64_BIT_INTEGER_VALUE, valueNode) |
@@ -32,3 +32,7 @@ const { GraphQLObjectType, GraphQLList, GraphQLInt } = require('graphql') | ||
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) | ||
@@ -42,3 +46,3 @@ | ||
if (type) { | ||
const field = { type } | ||
const field = { type, description: element.doc } | ||
if (element.is2many) field.args = argsGenerator(cache).generateArgumentsForType(element) | ||
@@ -45,0 +49,0 @@ fields[gqlName(name)] = field |
@@ -6,5 +6,9 @@ const hasScalarFields = entity => | ||
const _isFlatForeignKey = element => Boolean(element['@odata.foreignKey4'] || element._foreignKey4) | ||
const _isLocalized = element => element.name === 'localized' && element.target?.endsWith('.texts') | ||
const shouldElementBeIgnored = element => element.name.startsWith('up_') || _isLocalized(element) | ||
// TODO: add check for @cds.api.ignore | ||
const shouldElementBeIgnored = element => | ||
element.name.startsWith('up_') || _isLocalized(element) || _isFlatForeignKey(element) | ||
@@ -11,0 +15,0 @@ const isCompositionOfAspect = entity => |
{ | ||
"name": "@cap-js/graphql", | ||
"version": "0.9.0", | ||
"version": "0.10.0", | ||
"description": "CDS protocol adapter for GraphQL", | ||
@@ -47,3 +47,3 @@ "keywords": [ | ||
"jest": "^29.3.1", | ||
"prettier": "^3", | ||
"prettier": "3.2.4", | ||
"semver": "^7.4.0", | ||
@@ -50,0 +50,0 @@ "sqlite3": "^5.0.2" |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
95149
1626