Skip to content

Commit 7509281

Browse files
authored
Support more GraphQL versions (#3)
This PR overhauls the project so that it can support more versions of GraphQL. It tests it using GraphQL v15, v16 and v17 plus the semantic-non-null supporting branch `graphql@canary-pr-4192` (from graphql/graphql-js#4192). It also changes the logic around applying `@semanticNonNull` to an already non-null field (in this case, the non-null wins and the semantic is ignored). Further it exports a new method, `convertFieldConfig`, which can be used to convert a lone GraphQLFieldConfig object to nullable or strict mode based on its embedded `@semanticNonNull` directive (via astNode).
2 parents c281c0a + 4013319 commit 7509281

19 files changed

+478
-271
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212

1313
strategy:
1414
matrix:
15-
node-version: [20.x]
15+
node-version: [20.x, 22.x]
1616

1717
steps:
1818
- uses: actions/checkout@v3

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ For the directive, the two conversions work like this:
4747
| semantic-to-nullable | `[Int] @semanticNonNull(levels: [0,1])` | `[Int]` |
4848
| semantic-to-strict | `[Int] @semanticNonNull(levels: [0,1])` | `[Int!]!` |
4949

50+
> [!NOTE]
51+
>
52+
> An existing strictly non-nullable type (`Int!`) will remain unchanged whether
53+
> or not `@semanticNonNull` applies to that level.
54+
5055
### `GraphQLSemanticNonNull` wrapper type
5156

5257
How the `GraphQLSemanticNonNull` type is represented syntactically in SDL is yet
@@ -187,3 +192,20 @@ import { schema as sourceSchema } from "./my-schema";
187192
188193
export const schema = semanticToStrict(sourceSchema);
189194
```
195+
196+
## Advanced usage
197+
198+
If you just want to convert a single `GraphQLFieldConfig` you can use the
199+
`convertFieldConfig` method, passing the field config and `true` to convert
200+
semantic non-null positions to strict non-nulls, or `false` if you want to
201+
convert to nullable:
202+
203+
```ts
204+
const strictFieldConfig = convertFieldConfig(fieldConfig, true);
205+
const nullableFieldConfig = convertFieldConfig(fieldConfig, false);
206+
```
207+
208+
> [!NOTE]
209+
>
210+
> This method assumes that the fieldConfig has come from parsing an SDL string,
211+
> and thus has an `astNode` that includes a `@semanticNonNull` directive.

__tests__/graphql-pr-4192.test.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { runTest } from "./runTest.mjs";
2+
3+
runTest("graphql-pr-4192");

__tests__/graphql15.test.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { runTest } from "./runTest.mjs";
2+
3+
runTest("graphql15");

__tests__/graphql16.test.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { runTest } from "./runTest.mjs";
2+
3+
runTest("graphql16");

__tests__/graphql17.test.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { runTest } from "./runTest.mjs";
2+
3+
runTest("graphql17");

__tests__/index.test.mjs

-43
This file was deleted.

__tests__/runTest.mjs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// @ts-check
2+
3+
import * as assert from "node:assert";
4+
import { readdir, readFile } from "node:fs/promises";
5+
import { test } from "node:test";
6+
7+
const TEST_DIR = import.meta.dirname;
8+
const files = await readdir(TEST_DIR);
9+
const skip = test.skip.bind(test);
10+
11+
/** @param graphqlModuleName {string} */
12+
export const runTest = async (graphqlModuleName) => {
13+
test(graphqlModuleName, async (t) => {
14+
const mod = await import(graphqlModuleName);
15+
const { default: defaultExport, ...namedExports } = mod;
16+
const mockGraphql = t.mock.module("graphql", {
17+
cache: true,
18+
defaultExport,
19+
namedExports,
20+
});
21+
const graphql = await import("graphql");
22+
const { buildSchema, printSchema } = graphql;
23+
const isSemanticNonNullType = /** @type {any} */ (graphql)
24+
.isSemanticNonNullType;
25+
26+
const { semanticToNullable, semanticToStrict } = await import(
27+
`../dist/index.js?graphql=${graphqlModuleName}`
28+
);
29+
30+
for (const file of files) {
31+
if (file.endsWith(".test.graphql") && !file.startsWith(".")) {
32+
const pureDirective =
33+
file === "schema-with-directive-only.test.graphql";
34+
const maybeTest =
35+
pureDirective || isSemanticNonNullType != null ? test : skip;
36+
await maybeTest(file.replace(/\.test\.graphql$/, ""), async () => {
37+
const sdl = await readFile(TEST_DIR + "/" + file, "utf8");
38+
const schema = buildSchema(sdl);
39+
await test("semantic-to-strict", async () => {
40+
const expectedSdl = await readFile(
41+
TEST_DIR + "/snapshots/" + file.replace(".test.", ".strict."),
42+
"utf8",
43+
);
44+
const converted = semanticToStrict(schema);
45+
assert.equal(
46+
printSchema(converted).trim(),
47+
expectedSdl.trim(),
48+
"Expected semantic-to-strict to match",
49+
);
50+
});
51+
await test("semantic-to-nullable", async () => {
52+
const expectedSdl = await readFile(
53+
TEST_DIR + "/snapshots/" + file.replace(".test.", ".nullable."),
54+
"utf8",
55+
);
56+
const converted = semanticToNullable(schema);
57+
assert.equal(
58+
printSchema(converted).trim(),
59+
expectedSdl.trim(),
60+
"Expected semantic-to-nullable to match",
61+
);
62+
});
63+
});
64+
}
65+
}
66+
mockGraphql.restore();
67+
});
68+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
directive @semanticNonNull(levels: [Int!]) on FIELD_DEFINITION
2+
3+
type Query {
4+
allThings(includingArchived: Boolean, first: Int!): ThingConnection
5+
@semanticNonNull
6+
}
7+
8+
type ThingConnection {
9+
pageInfo: PageInfo!
10+
nodes: [Thing] @semanticNonNull(levels: [0, 1])
11+
}
12+
13+
type PageInfo {
14+
startCursor: String @semanticNonNull(levels: [0])
15+
endCursor: String @semanticNonNull
16+
hasNextPage: Boolean @semanticNonNull
17+
hasPreviousPage: Boolean @semanticNonNull
18+
}
19+
20+
interface Thing {
21+
id: ID!
22+
name: String @semanticNonNull
23+
description: String
24+
}
25+
26+
type Book implements Thing {
27+
id: ID!
28+
name: String @semanticNonNull
29+
description: String
30+
# Test that this non-null is retained
31+
pages: Int! @semanticNonNull
32+
}
33+
34+
type Car implements Thing {
35+
id: ID!
36+
name: String @semanticNonNull
37+
description: String
38+
mileage: Float @semanticNonNull
39+
}

__tests__/schema-with-directive.test.graphql

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type Book implements Thing {
2828
# Test that this semantic-non-null doesn't cause issues
2929
name: String* @semanticNonNull
3030
description: String
31-
# Test that this non-null gets stripped
31+
# Test that this non-null is retained
3232
pages: Int! @semanticNonNull
3333
}
3434

__tests__/schema.test.graphql

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type Book implements Thing {
2424
id: ID!
2525
name: String*
2626
description: String
27-
pages: Int*
27+
pages: Int!
2828
}
2929

3030
type Car implements Thing {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
type Query {
2+
allThings(includingArchived: Boolean, first: Int!): ThingConnection
3+
}
4+
5+
type ThingConnection {
6+
pageInfo: PageInfo!
7+
nodes: [Thing]
8+
}
9+
10+
type PageInfo {
11+
startCursor: String
12+
endCursor: String
13+
hasNextPage: Boolean
14+
hasPreviousPage: Boolean
15+
}
16+
17+
interface Thing {
18+
id: ID!
19+
name: String
20+
description: String
21+
}
22+
23+
type Book implements Thing {
24+
id: ID!
25+
name: String
26+
description: String
27+
pages: Int!
28+
}
29+
30+
type Car implements Thing {
31+
id: ID!
32+
name: String
33+
description: String
34+
mileage: Float
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
type Query {
2+
allThings(includingArchived: Boolean, first: Int!): ThingConnection!
3+
}
4+
5+
type ThingConnection {
6+
pageInfo: PageInfo!
7+
nodes: [Thing!]!
8+
}
9+
10+
type PageInfo {
11+
startCursor: String!
12+
endCursor: String!
13+
hasNextPage: Boolean!
14+
hasPreviousPage: Boolean!
15+
}
16+
17+
interface Thing {
18+
id: ID!
19+
name: String!
20+
description: String
21+
}
22+
23+
type Book implements Thing {
24+
id: ID!
25+
name: String!
26+
description: String
27+
pages: Int!
28+
}
29+
30+
type Car implements Thing {
31+
id: ID!
32+
name: String!
33+
description: String
34+
mileage: Float!
35+
}

__tests__/snapshots/schema-with-directive.nullable.graphql

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type Book implements Thing {
2424
id: ID!
2525
name: String
2626
description: String
27-
pages: Int
27+
pages: Int!
2828
}
2929

3030
type Car implements Thing {

__tests__/snapshots/schema.nullable.graphql

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type Book implements Thing {
2424
id: ID!
2525
name: String
2626
description: String
27-
pages: Int
27+
pages: Int!
2828
}
2929

3030
type Car implements Thing {

package.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
"scripts": {
1111
"prepack": "tsc && chmod +x dist/cli/*.js",
12-
"test": "node --test",
12+
"test": "node --test --experimental-test-module-mocks",
1313
"watch": "tsc --watch",
1414
"lint": "yarn prettier:check && eslint --ext .js,.jsx,.ts,.tsx,.graphql .",
1515
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx,.graphql . --fix; prettier --cache --ignore-path .eslintignore --write '**/*.{js,jsx,ts,tsx,graphql,md,json}'",
@@ -46,7 +46,7 @@
4646
"author": "Benjie Gillam <[email protected]>",
4747
"license": "MIT",
4848
"dependencies": {
49-
"graphql": "16.9.0-canary.pr.4192.1813397076f44a55e5798478e7321db9877de97a"
49+
"graphql": "15.x | 16.x | 17.x"
5050
},
5151
"devDependencies": {
5252
"@tsconfig/recommended": "^1.0.7",
@@ -58,6 +58,10 @@
5858
"eslint-plugin-import": "^2.28.1",
5959
"eslint-plugin-simple-import-sort": "^10.0.0",
6060
"eslint_d": "^13.0.0",
61+
"graphql-pr-4192": "npm:graphql@16.9.0-canary.pr.4192.1813397076f44a55e5798478e7321db9877de97a",
62+
"graphql15": "npm:[email protected]",
63+
"graphql16": "npm:[email protected]",
64+
"graphql17": "npm:[email protected]",
6165
"prettier": "^3.3.3",
6266
"typescript": "^5.6.2"
6367
},

0 commit comments

Comments
 (0)