Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Honour @id directive on type fields #60

Merged
merged 3 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 51 additions & 36 deletions src/schemaModelValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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`);
Expand All @@ -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)}`);
Expand Down Expand Up @@ -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) {

Expand All @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions src/test/directive-id.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type User {
userId: ID! @id
firstName: String
lastName: String
}

type Group {
name: String
}
54 changes: 54 additions & 0 deletions src/test/schemaModelValidator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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 userType = model.definitions.find(
def =>
def.kind === 'ObjectTypeDefinition' && def.name.value === 'User'
);
const userInputType = model.definitions.find(
def =>
def.kind === 'InputObjectTypeDefinition' && def.name.value === 'UserInput'
);

const userIdFields = getIdFields(userType);
const userInputIdFields = getIdFields(userInputType);

expect(userIdFields).toHaveLength(1);
expect(userInputIdFields).toHaveLength(1);
expect(userIdFields[0].name.value).toEqual(userInputIdFields[0].name.value);
});

function getIdFields(objTypeDef) {
return objTypeDef.fields.filter(
field =>
field.directives.some(directive => directive.name.value === 'id')
);
}
});