Skip to content

Commit 9a3de7f

Browse files
authored
Honour @id directive on type fields (#60)
When the `@id` directive was specified on a field, it was ignored by `schemaModelValidator` which added a new ID field, resulting in 2 ID fields. Now, the directive inference checks for the existence of an ID field before adding the default one. - break out logic around ID/input fields into dedicated functions - better inlining of input fields and values closes #59
1 parent 983c338 commit 9a3de7f

File tree

3 files changed

+118
-36
lines changed

3 files changed

+118
-36
lines changed

src/schemaModelValidator.js

+51-36
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ permissions and limitations under the License.
1111
*/
1212

1313
import { schemaStringify } from './schemaParser.js';
14+
import { print } from 'graphql';
1415
import {gql} from 'graphql-tag'
1516
import { loggerInfo, yellow } from "./logger.js";
1617

@@ -135,31 +136,10 @@ function injectChanges(schemaModel) {
135136

136137
function addNode(def) {
137138
let name = def.name.value;
138-
139-
// Input fields
140-
let inputFields = '';
141-
inputFields += `\n _id: ID @id`
142-
def.fields.forEach(field => {
143-
try {
144-
if (field.name.value === 'id') {
145-
inputFields += `\n id: ID`;
146-
}
147-
148-
} catch {}
149-
150-
try {
151-
if (field.type.name.value === 'String' ||
152-
field.type.name.value === 'Int' ||
153-
field.type.name.value === 'Float' ||
154-
field.type.name.value === 'Boolean') {
155-
156-
inputFields += `\n ${field.name.value}: ${field.type.name.value}`;
157-
}
158-
} catch {}
159-
});
139+
const idField = getIdField(def);
160140

161141
// Create Input type
162-
typesToAdd.push(`input ${name}Input {${inputFields}\n}`);
142+
typesToAdd.push(`input ${name}Input {\n${print(getInputFields(def))}\n}`);
163143

164144
// Create query
165145
queriesToAdd.push(`getNode${name}(filter: ${name}Input, options: Options): ${name}\n`);
@@ -168,7 +148,7 @@ function addNode(def) {
168148
// Create mutation
169149
mutationsToAdd.push(`createNode${name}(input: ${name}Input!): ${name}\n`);
170150
mutationsToAdd.push(`updateNode${name}(input: ${name}Input!): ${name}\n`);
171-
mutationsToAdd.push(`deleteNode${name}(_id: ID!): Boolean\n`);
151+
mutationsToAdd.push(`deleteNode${name}(${print(idFieldToInputValue(idField))}): Boolean\n`);
172152

173153
loggerInfo(`Added input type: ${yellow(name+'Input')}`);
174154
loggerInfo(`Added query: ${yellow('getNode' + name)}`);
@@ -231,6 +211,47 @@ function addFilterOptionsArguments(field) {
231211
}
232212

233213

214+
function getIdField(objTypeDef) {
215+
return objTypeDef.fields.find(
216+
field =>
217+
field.directives && field.directives.some(directive => directive.name.value === 'id')
218+
);
219+
}
220+
221+
222+
function createIdField() {
223+
return {
224+
kind: 'FieldDefinition',
225+
name: { kind: 'Name', value: '_id' },
226+
arguments: [],
227+
type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } } },
228+
directives: [
229+
{ kind: 'Directive', name: { kind: 'Name', value: 'id' }, arguments: [] }
230+
]
231+
};
232+
}
233+
234+
235+
function idFieldToInputValue({ name, type }) {
236+
return { kind: 'InputValueDefinition', name, type };
237+
}
238+
239+
240+
function getInputFields(objTypeDef) {
241+
return objTypeDef.fields.filter(field => isScalar(nullable(field.type)));
242+
}
243+
244+
245+
function nullable(type) {
246+
return type.kind === 'NonNullType' ? type.type : type;
247+
}
248+
249+
250+
function isScalar(type) {
251+
const scalarTypes = ['String', 'Int', 'Float', 'Boolean', 'ID'];
252+
return type.kind === 'NamedType' && scalarTypes.includes(type.name.value);
253+
}
254+
234255

235256
function inferGraphDatabaseDirectives(schemaModel) {
236257

@@ -242,21 +263,15 @@ function inferGraphDatabaseDirectives(schemaModel) {
242263
if (def.kind == 'ObjectTypeDefinition') {
243264
if (!(def.name.value == 'Query' || def.name.value == 'Mutation')) {
244265
currentType = def.name.value;
266+
267+
// Only add _id field to the object type if it doesn't have an ID field already
268+
if (!getIdField(def)) {
269+
def.fields.unshift(createIdField());
270+
}
271+
245272
addNode(def);
246273
const edgesTypeToAdd = [];
247274

248-
// Add _id field to the object type
249-
def.fields.unshift({
250-
kind: "FieldDefinition", name: { kind: "Name", value: "_id" },
251-
arguments: [],
252-
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "ID" }}},
253-
directives: [
254-
{ kind: "Directive", name: { kind: "Name", value: "id" },
255-
arguments: []
256-
}
257-
]
258-
});
259-
260275
// add relationships
261276
def.fields.forEach(field => {
262277
if (field.type.type !== undefined) {

src/test/directive-id.graphql

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
type User {
2+
userId: ID! @id
3+
firstName: String
4+
lastName: String
5+
}
6+
7+
type Group {
8+
name: String
9+
}

src/test/schemaModelValidator.test.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { readFileSync } from 'node:fs';
2+
import { loggerInit } from '../logger.js';
3+
import { validatedSchemaModel } from '../schemaModelValidator.js';
4+
import { schemaParser } from '../schemaParser.js';
5+
6+
describe('validatedSchemaModel', () => {
7+
let model;
8+
9+
beforeAll(() => {
10+
loggerInit('./output', false, 'silent');
11+
12+
const schema = readFileSync('./src/test/directive-id.graphql');
13+
model = validatedSchemaModel(schemaParser(schema));
14+
});
15+
16+
test('should only add _id field to object types without ID fields', () => {
17+
const objTypeDefs = model.definitions.filter(def => def.kind === 'ObjectTypeDefinition');
18+
const userType = objTypeDefs.find(def => def.name.value === 'User');
19+
const groupType = objTypeDefs.find(def => def.name.value === 'Group');
20+
21+
const userIdFields = getIdFields(userType);
22+
const groupIdFields = getIdFields(groupType);
23+
24+
expect(userIdFields).toHaveLength(1);
25+
expect(groupIdFields).toHaveLength(1);
26+
expect(userIdFields[0].name.value).toEqual('userId');
27+
expect(groupIdFields[0].name.value).toEqual('_id');
28+
});
29+
30+
test('should define the same ID fields on a type and its input type', () => {
31+
const typeNames = ['User', 'Group'];
32+
33+
typeNames.forEach(typeName => {
34+
const type = model.definitions.find(
35+
def =>
36+
def.kind === 'ObjectTypeDefinition' && def.name.value === typeName
37+
);
38+
const inputType = model.definitions.find(
39+
def =>
40+
def.kind === 'InputObjectTypeDefinition' && def.name.value === `${typeName}Input`
41+
);
42+
43+
const idFields = getIdFields(type);
44+
const inputIdFields = getIdFields(inputType);
45+
46+
expect(idFields).toHaveLength(1);
47+
expect(inputIdFields).toHaveLength(1);
48+
expect(idFields[0].name.value).toEqual(inputIdFields[0].name.value);
49+
});
50+
});
51+
52+
function getIdFields(objTypeDef) {
53+
return objTypeDef.fields.filter(
54+
field =>
55+
field.directives.some(directive => directive.name.value === 'id')
56+
);
57+
}
58+
});

0 commit comments

Comments
 (0)