Skip to content

Support more GraphQL versions #3

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

Merged
merged 14 commits into from
Mar 19, 2025
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
node-version: [20.x]
node-version: [20.x, 22.x]

steps:
- uses: actions/checkout@v3
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ For the directive, the two conversions work like this:
| semantic-to-nullable | `[Int] @semanticNonNull(levels: [0,1])` | `[Int]` |
| semantic-to-strict | `[Int] @semanticNonNull(levels: [0,1])` | `[Int!]!` |

> [!NOTE]
>
> An existing strictly non-nullable type (`Int!`) will remain unchanged whether
> or not `@semanticNonNull` applies to that level.

### `GraphQLSemanticNonNull` wrapper type

How the `GraphQLSemanticNonNull` type is represented syntactically in SDL is yet
Expand Down Expand Up @@ -187,3 +192,20 @@ import { schema as sourceSchema } from "./my-schema";

export const schema = semanticToStrict(sourceSchema);
```

## Advanced usage

If you just want to convert a single `GraphQLFieldConfig` you can use the
`convertFieldConfig` method, passing the field config and `true` to convert
semantic non-null positions to strict non-nulls, or `false` if you want to
convert to nullable:

```ts
const strictFieldConfig = convertFieldConfig(fieldConfig, true);
const nullableFieldConfig = convertFieldConfig(fieldConfig, false);
```

> [!NOTE]
>
> This method assumes that the fieldConfig has come from parsing an SDL string,
> and thus has an `astNode` that includes a `@semanticNonNull` directive.
3 changes: 3 additions & 0 deletions __tests__/graphql-pr-4192.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { runTest } from "./runTest.mjs";

runTest("graphql-pr-4192");
3 changes: 3 additions & 0 deletions __tests__/graphql15.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { runTest } from "./runTest.mjs";

runTest("graphql15");
3 changes: 3 additions & 0 deletions __tests__/graphql16.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { runTest } from "./runTest.mjs";

runTest("graphql16");
3 changes: 3 additions & 0 deletions __tests__/graphql17.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { runTest } from "./runTest.mjs";

runTest("graphql17");
43 changes: 0 additions & 43 deletions __tests__/index.test.mjs

This file was deleted.

68 changes: 68 additions & 0 deletions __tests__/runTest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// @ts-check

import * as assert from "node:assert";
import { readdir, readFile } from "node:fs/promises";
import { test } from "node:test";

const TEST_DIR = import.meta.dirname;
const files = await readdir(TEST_DIR);
const skip = test.skip.bind(test);

/** @param graphqlModuleName {string} */
export const runTest = async (graphqlModuleName) => {
test(graphqlModuleName, async (t) => {
const mod = await import(graphqlModuleName);
const { default: defaultExport, ...namedExports } = mod;
const mockGraphql = t.mock.module("graphql", {
cache: true,
defaultExport,
namedExports,
});
const graphql = await import("graphql");
const { buildSchema, printSchema } = graphql;
const isSemanticNonNullType = /** @type {any} */ (graphql)
.isSemanticNonNullType;

const { semanticToNullable, semanticToStrict } = await import(
`../dist/index.js?graphql=${graphqlModuleName}`
);

for (const file of files) {
if (file.endsWith(".test.graphql") && !file.startsWith(".")) {
const pureDirective =
file === "schema-with-directive-only.test.graphql";
const maybeTest =
pureDirective || isSemanticNonNullType != null ? test : skip;
await maybeTest(file.replace(/\.test\.graphql$/, ""), async () => {
const sdl = await readFile(TEST_DIR + "/" + file, "utf8");
const schema = buildSchema(sdl);
await test("semantic-to-strict", async () => {
const expectedSdl = await readFile(
TEST_DIR + "/snapshots/" + file.replace(".test.", ".strict."),
"utf8",
);
const converted = semanticToStrict(schema);
assert.equal(
printSchema(converted).trim(),
expectedSdl.trim(),
"Expected semantic-to-strict to match",
);
});
await test("semantic-to-nullable", async () => {
const expectedSdl = await readFile(
TEST_DIR + "/snapshots/" + file.replace(".test.", ".nullable."),
"utf8",
);
const converted = semanticToNullable(schema);
assert.equal(
printSchema(converted).trim(),
expectedSdl.trim(),
"Expected semantic-to-nullable to match",
);
});
});
}
}
mockGraphql.restore();
});
};
39 changes: 39 additions & 0 deletions __tests__/schema-with-directive-only.test.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
directive @semanticNonNull(levels: [Int!]) on FIELD_DEFINITION

type Query {
allThings(includingArchived: Boolean, first: Int!): ThingConnection
@semanticNonNull
}

type ThingConnection {
pageInfo: PageInfo!
nodes: [Thing] @semanticNonNull(levels: [0, 1])
}

type PageInfo {
startCursor: String @semanticNonNull(levels: [0])
endCursor: String @semanticNonNull
hasNextPage: Boolean @semanticNonNull
hasPreviousPage: Boolean @semanticNonNull
}

interface Thing {
id: ID!
name: String @semanticNonNull
description: String
}

type Book implements Thing {
id: ID!
name: String @semanticNonNull
description: String
# Test that this non-null is retained
pages: Int! @semanticNonNull
}

type Car implements Thing {
id: ID!
name: String @semanticNonNull
description: String
mileage: Float @semanticNonNull
}
2 changes: 1 addition & 1 deletion __tests__/schema-with-directive.test.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type Book implements Thing {
# Test that this semantic-non-null doesn't cause issues
name: String* @semanticNonNull
description: String
# Test that this non-null gets stripped
# Test that this non-null is retained
pages: Int! @semanticNonNull
}

Expand Down
2 changes: 1 addition & 1 deletion __tests__/schema.test.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Book implements Thing {
id: ID!
name: String*
description: String
pages: Int*
pages: Int!
}

type Car implements Thing {
Expand Down
35 changes: 35 additions & 0 deletions __tests__/snapshots/schema-with-directive-only.nullable.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
type Query {
allThings(includingArchived: Boolean, first: Int!): ThingConnection
}

type ThingConnection {
pageInfo: PageInfo!
nodes: [Thing]
}

type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean
hasPreviousPage: Boolean
}

interface Thing {
id: ID!
name: String
description: String
}

type Book implements Thing {
id: ID!
name: String
description: String
pages: Int!
}

type Car implements Thing {
id: ID!
name: String
description: String
mileage: Float
}
35 changes: 35 additions & 0 deletions __tests__/snapshots/schema-with-directive-only.strict.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
type Query {
allThings(includingArchived: Boolean, first: Int!): ThingConnection!
}

type ThingConnection {
pageInfo: PageInfo!
nodes: [Thing!]!
}

type PageInfo {
startCursor: String!
endCursor: String!
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}

interface Thing {
id: ID!
name: String!
description: String
}

type Book implements Thing {
id: ID!
name: String!
description: String
pages: Int!
}

type Car implements Thing {
id: ID!
name: String!
description: String
mileage: Float!
}
2 changes: 1 addition & 1 deletion __tests__/snapshots/schema-with-directive.nullable.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Book implements Thing {
id: ID!
name: String
description: String
pages: Int
pages: Int!
}

type Car implements Thing {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/snapshots/schema.nullable.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Book implements Thing {
id: ID!
name: String
description: String
pages: Int
pages: Int!
}

type Car implements Thing {
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"scripts": {
"prepack": "tsc && chmod +x dist/cli/*.js",
"test": "node --test",
"test": "node --test --experimental-test-module-mocks",
"watch": "tsc --watch",
"lint": "yarn prettier:check && eslint --ext .js,.jsx,.ts,.tsx,.graphql .",
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx,.graphql . --fix; prettier --cache --ignore-path .eslintignore --write '**/*.{js,jsx,ts,tsx,graphql,md,json}'",
Expand Down Expand Up @@ -46,7 +46,7 @@
"author": "Benjie Gillam <code@benjiegillam.com>",
"license": "MIT",
"dependencies": {
"graphql": "16.9.0-canary.pr.4192.1813397076f44a55e5798478e7321db9877de97a"
"graphql": "15.x | 16.x | 17.x"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.7",
Expand All @@ -58,6 +58,10 @@
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint_d": "^13.0.0",
"graphql-pr-4192": "npm:graphql@16.9.0-canary.pr.4192.1813397076f44a55e5798478e7321db9877de97a",
"graphql15": "npm:graphql@15.x",
"graphql16": "npm:graphql@16.x",
"graphql17": "npm:graphql@17.x",
"prettier": "^3.3.3",
"typescript": "^5.6.2"
},
Expand Down
Loading