Skip to content

Commit 0e32302

Browse files
add lexicographical sorting rules for schema stitching support (#256)
* fix sorting to be lexicographical In larger projects, GraphQL schema stitching is a standardized practice. When stitching, Query, Mutation, and Subscription types are not always merged together sorted. [graphql-js has provided a function lexicographicSortSchema](graphql/graphql-js#2036) to enable stitched schemas to be sorted at runtime. Originally, this library used default JS sorting which is alphabetical. This makes the alphabetical rules virtually unusable by larger projects. * introduce new listIsLexicographical function Rather than a breaking change, I'm proposing new rules as a feature. :) * add lexicographical versions of alphabetical rules alphabetical !== lexicographical in JS. Let's just support both from this lib for backwards compatibility reasons. * udpate README.md * add rulesOptions to configuration * update expect helpers to take configurationOptions * update listIsAlphabetical to support lexicographical order * remove lexicographic rules and update alphabetical rules to use sortOrder config option * update README.md and runner
1 parent ae0ce9b commit 0e32302

20 files changed

+443
-111
lines changed

README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ Options:
3434
3535
example: --rules fields-have-descriptions,types-have-descriptions
3636
37+
-o, --rules-options <rulesOptions>
38+
39+
configure the specified rules with the passed in configuration options
40+
41+
example: --rules-options '{"enum-values-sorted-alphabetically":{"sortOrder":"lexicographical"}'
42+
3743
-i, --ignore <ignore list>
3844
3945
ignore errors for specific schema members (see "Inline rule overrides" for an alternative way to do this)
@@ -159,7 +165,10 @@ For now, only `rules`, `customRulePaths` and `schemaPaths` can be configured in
159165
"graphql-schema-linter": {
160166
"rules": ["enum-values-sorted-alphabetically"],
161167
"schemaPaths": ["path/to/my/schema/files/**.graphql"],
162-
"customRulePaths": ["path/to/my/custom/rules/*.js"]
168+
"customRulePaths": ["path/to/my/custom/rules/*.js"],
169+
"rulesOptions": {
170+
"enum-values-sorted-alphabetically": { "sortOrder": "lexicographical" }
171+
}
163172
}
164173
}
165174
```
@@ -170,7 +179,10 @@ For now, only `rules`, `customRulePaths` and `schemaPaths` can be configured in
170179
{
171180
"rules": ["enum-values-sorted-alphabetically"],
172181
"schemaPaths": ["path/to/my/schema/files/**.graphql"],
173-
"customRulePaths": ["path/to/my/custom/rules/*.js"]
182+
"customRulePaths": ["path/to/my/custom/rules/*.js"],
183+
"rulesOptions": {
184+
"enum-values-sorted-alphabetically": { "sortOrder": "lexicographical" }
185+
}
174186
}
175187
```
176188

@@ -181,6 +193,9 @@ module.exports = {
181193
rules: ['enum-values-sorted-alphabetically'],
182194
schemaPaths: ['path/to/my/schema/files/**.graphql'],
183195
customRulePaths: ['path/to/my/custom/rules/*.js'],
196+
rulesOptions: {
197+
'enum-values-sorted-alphabetically': { sortOrder: 'lexicographical' }
198+
}
184199
};
185200
```
186201

@@ -252,7 +267,9 @@ This rule will validate that all enum values have a description.
252267

253268
### `enum-values-sorted-alphabetically`
254269

255-
This rule will validate that all enum values are sorted alphabetically.
270+
This rule will validate that all enum values are sorted alphabetically. Accepts following rule options:
271+
272+
- sortOrder: <String> - either 'alphabetical' or 'lexicographical', defaults: 'alphabetical'
256273

257274
### `fields-are-camel-cased`
258275

@@ -264,7 +281,9 @@ This rule will validate that object type fields and interface type fields have a
264281

265282
### `input-object-fields-sorted-alphabetically`
266283

267-
This rule will validate that all input object fields are sorted alphabetically.
284+
This rule will validate that all input object fields are sorted alphabetically. Accepts following rule options:
285+
286+
- sortOrder: <String> - either 'alphabetical' or 'lexicographical', defaults: 'alphabetical'
268287

269288
### `input-object-values-are-camel-cased`
270289

@@ -306,7 +325,9 @@ More specifically:
306325

307326
### `type-fields-sorted-alphabetically`
308327

309-
This rule will validate that all type object fields are sorted alphabetically.
328+
This rule will validate that all type object fields are sorted alphabetically. Accepts following rule options:
329+
330+
- sortOrder: <String> - either 'alphabetical' or 'lexicographical', defaults: 'alphabetical'
310331

311332
### `types-are-capitalized`
312333

src/configuration.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export class Configuration {
1010
options:
1111
- format: (required) `text` | `json`
1212
- rules: [string array] whitelist rules
13+
- rulesOptions: [string to object] configuration options for rules. Example: "rulesOptions": { "enum-values-sorted-alphabetically": { "sortOrder": "lexicographical" } }
1314
- ignore: [string to string array object] ignore list for rules. Example: {"fields-have-descriptions": ["Obvious", "Query.obvious", "Query.something.obvious"]}
1415
- customRulePaths: [string array] path to additional custom rules to be loaded
1516
- commentDescriptions: [boolean] use old way of defining descriptions in GraphQL SDL
@@ -21,6 +22,7 @@ export class Configuration {
2122
customRulePaths: [],
2223
commentDescriptions: false,
2324
oldImplementsSyntax: false,
25+
rulesOptions: {},
2426
ignore: {},
2527
};
2628

@@ -113,6 +115,10 @@ export class Configuration {
113115
return this.getRulesFromPaths([this.builtInRulePaths]);
114116
}
115117

118+
getRulesOptions() {
119+
return this.options.rulesOptions;
120+
}
121+
116122
getIgnoreList() {
117123
return this.options.ignore;
118124
}

src/options.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function loadOptionsFromConfigDir(configDirectory) {
2929

3030
return {
3131
rules: cosmic.config.rules,
32+
rulesOptions: cosmic.config.rulesOptions || {},
3233
ignore: cosmic.config.ignore || {},
3334
customRulePaths: customRulePaths || [],
3435
schemaPaths: schemaPaths,

src/rules/enum_values_sorted_alphabetically.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
import { ValidationError } from '../validation_error';
22
import listIsAlphabetical from '../util/listIsAlphabetical';
33

4-
export function EnumValuesSortedAlphabetically(context) {
4+
export function EnumValuesSortedAlphabetically(configuration, context) {
5+
const ruleKey = 'enum-values-sorted-alphabetically';
56
return {
67
EnumTypeDefinition(node, key, parent, path, ancestors) {
78
const enumValues = node.values.map((val) => {
89
return val.name.value;
910
});
1011

11-
const { isSorted, sortedList } = listIsAlphabetical(enumValues);
12+
const { sortOrder = 'alphabetical' } =
13+
configuration.getRulesOptions()[ruleKey] || {};
14+
const { isSorted, sortedList } = listIsAlphabetical(
15+
enumValues,
16+
sortOrder
17+
);
1218

1319
if (!isSorted) {
1420
context.reportError(
1521
new ValidationError(
16-
'enum-values-sorted-alphabetically',
17-
`The enum \`${node.name.value}\` should be sorted alphabetically. ` +
22+
ruleKey,
23+
`The enum \`${node.name.value}\` should be sorted in ${sortOrder} order. ` +
1824
`Expected sorting: ${sortedList.join(', ')}`,
1925
[node]
2026
)

src/rules/input_object_fields_sorted_alphabetically.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import { ValidationError } from '../validation_error';
22
import listIsAlphabetical from '../util/listIsAlphabetical';
33

4-
export function InputObjectFieldsSortedAlphabetically(context) {
4+
export function InputObjectFieldsSortedAlphabetically(configuration, context) {
5+
const ruleKey = 'input-object-fields-sorted-alphabetically';
56
return {
67
InputObjectTypeDefinition(node) {
78
const fieldList = (node.fields || []).map((field) => field.name.value);
8-
const { isSorted, sortedList } = listIsAlphabetical(fieldList);
9+
10+
const { sortOrder = 'alphabetical' } =
11+
configuration.getRulesOptions()[ruleKey] || {};
12+
const { isSorted, sortedList } = listIsAlphabetical(fieldList, sortOrder);
913

1014
if (!isSorted) {
1115
context.reportError(
1216
new ValidationError(
13-
'input-object-fields-sorted-alphabetically',
14-
`The fields of input type \`${node.name.value}\` should be sorted alphabetically. ` +
17+
ruleKey,
18+
`The fields of input type \`${node.name.value}\` should be sorted in ${sortOrder} order. ` +
1519
`Expected sorting: ${sortedList.join(', ')}`,
1620
[node]
1721
)

src/rules/type_fields_sorted_alphabetically.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import { ValidationError } from '../validation_error';
22
import listIsAlphabetical from '../util/listIsAlphabetical';
33

4-
export function TypeFieldsSortedAlphabetically(context) {
4+
export function TypeFieldsSortedAlphabetically(configuration, context) {
5+
const ruleKey = 'type-fields-sorted-alphabetically';
56
return {
67
ObjectTypeDefinition(node) {
78
const fieldList = (node.fields || []).map((field) => field.name.value);
8-
const { isSorted, sortedList } = listIsAlphabetical(fieldList);
9+
10+
const { sortOrder = 'alphabetical' } =
11+
configuration.getRulesOptions()[ruleKey] || {};
12+
const { isSorted, sortedList } = listIsAlphabetical(fieldList, sortOrder);
913

1014
if (!isSorted) {
1115
context.reportError(
1216
new ValidationError(
13-
'type-fields-sorted-alphabetically',
14-
`The fields of object type \`${node.name.value}\` should be sorted alphabetically. ` +
17+
ruleKey,
18+
`The fields of object type \`${node.name.value}\` should be sorted in ${sortOrder} order. ` +
1519
`Expected sorting: ${sortedList.join(', ')}`,
1620
[node]
1721
)

src/runner.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export async function run(stdout, stdin, stderr, argv) {
1414
'-r, --rules <rules>',
1515
'only the rules specified will be used to validate the schema. Example: fields-have-descriptions,types-have-descriptions'
1616
)
17+
.option(
18+
'-o, --rules-options <rulesOptions>',
19+
'configure the specified rules with the passed in configuration options. example: {"enum-values-sorted-alphabetically":{"sortOrder":"lexicographical"}}'
20+
)
1721
.option(
1822
'-i, --ignore <ignore list>',
1923
"ignore errors for specific schema members, example: {'fields-have-descriptions':['Obvious','Query.obvious','Query.something.obvious']}"
@@ -152,6 +156,10 @@ function getOptionsFromCommander(commander) {
152156
options.rules = commander.rules.split(',');
153157
}
154158

159+
if (commander.rulesOptions) {
160+
options.rulesOptions = JSON.parse(commander.rulesOptions);
161+
}
162+
155163
if (commander.ignore) {
156164
options.ignore = JSON.parse(commander.ignore);
157165
}

src/util/arraysEqual.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @summary Returns `true` if two arrays have the same item values in the same order.
3+
*/
4+
export default function arraysEqual(a, b) {
5+
for (var i = 0; i < a.length; ++i) {
6+
if (a[i] !== b[i]) return false;
7+
}
8+
return true;
9+
}

src/util/listIsAlphabetical.js

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
1-
/**
2-
* @summary Returns `true` if two arrays have the same item values in the same order.
3-
*/
4-
function arraysEqual(a, b) {
5-
for (var i = 0; i < a.length; ++i) {
6-
if (a[i] !== b[i]) return false;
7-
}
8-
return true;
9-
}
1+
import arraysEqual from './arraysEqual';
102

113
/**
124
* @summary Returns `true` if the list is in alphabetical order,
135
* or an alphabetized list if not
146
* @param {String[]} list Array of strings
157
* @return {Object} { isSorted: Bool, sortedList: String[] }
168
*/
17-
export default function listIsAlphabetical(list) {
18-
const sortedList = list.slice().sort();
9+
export default function listIsAlphabetical(list, sortOrder = 'alphabetical') {
10+
let sortFn;
11+
if (sortOrder === 'lexicographical') {
12+
sortFn = (a, b) => a.localeCompare(b);
13+
}
14+
15+
const sortedList = list.slice().sort(sortFn);
1916
return {
2017
isSorted: arraysEqual(list, sortedList),
2118
sortedList,

test/assertions.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,18 @@ const DefaultSchema = `
1414
}
1515
`;
1616

17-
export function expectFailsRule(rule, schemaSDL, expectedErrors = []) {
18-
return expectFailsRuleWithConfiguration(rule, schemaSDL, {}, expectedErrors);
17+
export function expectFailsRule(
18+
rule,
19+
schemaSDL,
20+
expectedErrors = [],
21+
configurationOptions = {}
22+
) {
23+
return expectFailsRuleWithConfiguration(
24+
rule,
25+
schemaSDL,
26+
configurationOptions,
27+
expectedErrors
28+
);
1929
}
2030

2131
export function expectFailsRuleWithConfiguration(
@@ -41,8 +51,8 @@ export function expectFailsRuleWithConfiguration(
4151
);
4252
}
4353

44-
export function expectPassesRule(rule, schemaSDL) {
45-
expectPassesRuleWithConfiguration(rule, schemaSDL, {});
54+
export function expectPassesRule(rule, schemaSDL, configurationOptions = {}) {
55+
expectPassesRuleWithConfiguration(rule, schemaSDL, configurationOptions);
4656
}
4757

4858
export function expectPassesRuleWithConfiguration(

0 commit comments

Comments
 (0)