diff --git a/src/schemaModelValidator.js b/src/schemaModelValidator.js index 3c59a20..4e9def8 100644 --- a/src/schemaModelValidator.js +++ b/src/schemaModelValidator.js @@ -11,6 +11,7 @@ permissions and limitations under the License. */ import { schemaStringify } from './schemaParser.js'; +import { print } from 'graphql'; import {gql} from 'graphql-tag' import { loggerInfo, yellow } from "./logger.js"; @@ -135,31 +136,10 @@ function injectChanges(schemaModel) { function addNode(def) { let name = def.name.value; - - // Input fields - let inputFields = ''; - inputFields += `\n _id: ID @id` - def.fields.forEach(field => { - try { - if (field.name.value === 'id') { - inputFields += `\n id: ID`; - } - - } catch {} - - try { - if (field.type.name.value === 'String' || - field.type.name.value === 'Int' || - field.type.name.value === 'Float' || - field.type.name.value === 'Boolean') { - - inputFields += `\n ${field.name.value}: ${field.type.name.value}`; - } - } catch {} - }); + const idField = getIdField(def); // Create Input type - typesToAdd.push(`input ${name}Input {${inputFields}\n}`); + typesToAdd.push(`input ${name}Input {\n${print(getInputFields(def))}\n}`); // Create query queriesToAdd.push(`getNode${name}(filter: ${name}Input, options: Options): ${name}\n`); @@ -168,7 +148,7 @@ function addNode(def) { // Create mutation mutationsToAdd.push(`createNode${name}(input: ${name}Input!): ${name}\n`); mutationsToAdd.push(`updateNode${name}(input: ${name}Input!): ${name}\n`); - mutationsToAdd.push(`deleteNode${name}(_id: ID!): Boolean\n`); + mutationsToAdd.push(`deleteNode${name}(${print(idFieldToInputValue(idField))}): Boolean\n`); loggerInfo(`Added input type: ${yellow(name+'Input')}`); loggerInfo(`Added query: ${yellow('getNode' + name)}`); @@ -231,6 +211,47 @@ function addFilterOptionsArguments(field) { } +function getIdField(objTypeDef) { + return objTypeDef.fields.find( + field => + field.directives && field.directives.some(directive => directive.name.value === 'id') + ); +} + + +function createIdField() { + return { + kind: 'FieldDefinition', + name: { kind: 'Name', value: '_id' }, + arguments: [], + type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } } }, + directives: [ + { kind: 'Directive', name: { kind: 'Name', value: 'id' }, arguments: [] } + ] + }; +} + + +function idFieldToInputValue({ name, type }) { + return { kind: 'InputValueDefinition', name, type }; +} + + +function getInputFields(objTypeDef) { + return objTypeDef.fields.filter(field => isScalar(nullable(field.type))); +} + + +function nullable(type) { + return type.kind === 'NonNullType' ? type.type : type; +} + + +function isScalar(type) { + const scalarTypes = ['String', 'Int', 'Float', 'Boolean', 'ID']; + return type.kind === 'NamedType' && scalarTypes.includes(type.name.value); +} + function inferGraphDatabaseDirectives(schemaModel) { @@ -242,21 +263,15 @@ function inferGraphDatabaseDirectives(schemaModel) { if (def.kind == 'ObjectTypeDefinition') { if (!(def.name.value == 'Query' || def.name.value == 'Mutation')) { currentType = def.name.value; + + // Only add _id field to the object type if it doesn't have an ID field already + if (!getIdField(def)) { + def.fields.unshift(createIdField()); + } + addNode(def); const edgesTypeToAdd = []; - // Add _id field to the object type - def.fields.unshift({ - kind: "FieldDefinition", name: { kind: "Name", value: "_id" }, - arguments: [], - type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "ID" }}}, - directives: [ - { kind: "Directive", name: { kind: "Name", value: "id" }, - arguments: [] - } - ] - }); - // add relationships def.fields.forEach(field => { if (field.type.type !== undefined) { diff --git a/src/test/directive-id.graphql b/src/test/directive-id.graphql new file mode 100644 index 0000000..972d07a --- /dev/null +++ b/src/test/directive-id.graphql @@ -0,0 +1,9 @@ +type User { + userId: ID! @id + firstName: String + lastName: String +} + +type Group { + name: String +} diff --git a/src/test/schemaModelValidator.test.js b/src/test/schemaModelValidator.test.js new file mode 100644 index 0000000..faeced5 --- /dev/null +++ b/src/test/schemaModelValidator.test.js @@ -0,0 +1,58 @@ +import { readFileSync } from 'node:fs'; +import { loggerInit } from '../logger.js'; +import { validatedSchemaModel } from '../schemaModelValidator.js'; +import { schemaParser } from '../schemaParser.js'; + +describe('validatedSchemaModel', () => { + let model; + + beforeAll(() => { + loggerInit('./output', false, 'silent'); + + const schema = readFileSync('./src/test/directive-id.graphql'); + model = validatedSchemaModel(schemaParser(schema)); + }); + + test('should only add _id field to object types without ID fields', () => { + const objTypeDefs = model.definitions.filter(def => def.kind === 'ObjectTypeDefinition'); + const userType = objTypeDefs.find(def => def.name.value === 'User'); + const groupType = objTypeDefs.find(def => def.name.value === 'Group'); + + const userIdFields = getIdFields(userType); + const groupIdFields = getIdFields(groupType); + + expect(userIdFields).toHaveLength(1); + expect(groupIdFields).toHaveLength(1); + expect(userIdFields[0].name.value).toEqual('userId'); + expect(groupIdFields[0].name.value).toEqual('_id'); + }); + + test('should define the same ID fields on a type and its input type', () => { + const typeNames = ['User', 'Group']; + + typeNames.forEach(typeName => { + const type = model.definitions.find( + def => + def.kind === 'ObjectTypeDefinition' && def.name.value === typeName + ); + const inputType = model.definitions.find( + def => + def.kind === 'InputObjectTypeDefinition' && def.name.value === `${typeName}Input` + ); + + const idFields = getIdFields(type); + const inputIdFields = getIdFields(inputType); + + expect(idFields).toHaveLength(1); + expect(inputIdFields).toHaveLength(1); + expect(idFields[0].name.value).toEqual(inputIdFields[0].name.value); + }); + }); + + function getIdFields(objTypeDef) { + return objTypeDef.fields.filter( + field => + field.directives.some(directive => directive.name.value === 'id') + ); + } +});