Skip to content

Commit 5b73106

Browse files
committed
Honour @id directive on type fields
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 f1d1498 commit 5b73106

File tree

3 files changed

+108
-36
lines changed

3 files changed

+108
-36
lines changed

src/schemaModelValidator.js

+45-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,11 @@ 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+
const inputFields = [idField, ...getInputFields(def)];
143+
typesToAdd.push(`input ${name}Input {\n${print(inputFields)}\n}`);
163144

164145
// Create query
165146
queriesToAdd.push(`getNode${name}(filter: ${name}Input, options: Options): ${name}\n`);
@@ -168,7 +149,7 @@ function addNode(def) {
168149
// Create mutation
169150
mutationsToAdd.push(`createNode${name}(input: ${name}Input!): ${name}\n`);
170151
mutationsToAdd.push(`updateNode${name}(input: ${name}Input!): ${name}\n`);
171-
mutationsToAdd.push(`deleteNode${name}(_id: ID!): Boolean\n`);
152+
mutationsToAdd.push(`deleteNode${name}(${print(idFieldToInputValue(idField))}): Boolean\n`);
172153

173154
loggerInfo(`Added input type: ${yellow(name+'Input')}`);
174155
loggerInfo(`Added query: ${yellow('getNode' + name)}`);
@@ -231,6 +212,40 @@ function addFilterOptionsArguments(field) {
231212
}
232213

233214

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

235250
function inferGraphDatabaseDirectives(schemaModel) {
236251

@@ -242,21 +257,15 @@ function inferGraphDatabaseDirectives(schemaModel) {
242257
if (def.kind == 'ObjectTypeDefinition') {
243258
if (!(def.name.value == 'Query' || def.name.value == 'Mutation')) {
244259
currentType = def.name.value;
260+
261+
// Only add _id field to the object type if it doesn't have an ID field already
262+
if (!getIdField(def)) {
263+
def.fields.unshift(createIdField());
264+
}
265+
245266
addNode(def);
246267
const edgesTypeToAdd = [];
247268

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-
260269
// add relationships
261270
def.fields.forEach(field => {
262271
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

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 userType = model.definitions.find(
32+
def =>
33+
def.kind === 'ObjectTypeDefinition' && def.name.value === 'User'
34+
);
35+
const userInputType = model.definitions.find(
36+
def =>
37+
def.kind === 'InputObjectTypeDefinition' && def.name.value === 'UserInput'
38+
);
39+
40+
const userIdFields = getIdFields(userType);
41+
const userInputIdFields = getIdFields(userInputType);
42+
43+
expect(userIdFields).toHaveLength(1);
44+
expect(userInputIdFields).toHaveLength(1);
45+
expect(userIdFields[0].name.value).toEqual(userInputIdFields[0].name.value);
46+
});
47+
48+
function getIdFields(objTypeDef) {
49+
return objTypeDef.fields.filter(
50+
field =>
51+
field.directives.some(directive => directive.name.value === 'id')
52+
);
53+
}
54+
});

0 commit comments

Comments
 (0)