Skip to content

Commit 905f540

Browse files
committed
Use Schema Coordinates for locate
Rewrite the `locate` command to use graphql-js v17's `resolveSchemaCoordinate()` API instead of the hand-rolled entity name parser. This adds support for all schema coordinate forms defined in the spec (https://spec.graphql.org/draft/#sec-Schema-Coordinates): - Type, Type.field, Type.field(arg:) - EnumType.VALUE, InputType.field - @directive, @directive(arg:) Also adds test fixtures for enum values and field arguments, and updates CLI help text and documentation.
1 parent a394240 commit 905f540

12 files changed

Lines changed: 202 additions & 75 deletions

llm-docs/getting-started/cli.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,30 +55,42 @@ Options:
5555
-h, --help display help for command
5656
5757
Commands:
58-
locate [options] <ENTITY>
58+
locate [options] <COORDINATE>
5959
```
6060

6161
## Locate
6262

63-
The `locate` command reports the location (file, line, column) at which a given type or field is defined in your code. `grats locate` can also be invoked by other tools. For example the click-to-definition feature of an GraphQL editor integration could use invoke this command to find the location of a type or field.
63+
The `locate` command reports the location (file, line, column) at which a given schema element is defined in your code. It accepts a [Schema Coordinate](https://spec.graphql.org/draft/#sec-Schema-Coordinates) as its argument.
64+
65+
`grats locate` can also be invoked by other tools. For example the click-to-definition feature of a GraphQL editor integration could invoke this command to find the location of a type or field.
6466

6567
For example, Relay's VSCode Extension is [exploring](https://github.com/facebook/relay/pull/4434) adding the ability to leverage such a tool.
6668

6769
```bash
70+
# Locate a named type
71+
npx grats locate User
72+
6873
# Locate a field
6974
npx grats locate User.name
7075

71-
# Locate a named type
72-
npx grats locate User
76+
# Locate a field argument
77+
npx grats locate "Query.user(id:)"
78+
79+
# Locate an enum value
80+
npx grats locate "MyEnum.VALUE"
81+
82+
# Locate a directive
83+
npx grats locate @deprecated
7384
```
7485

7586
### Options
7687

7788
```text
78-
Usage: grats locate [options] <ENTITY>
89+
Usage: grats locate [options] <COORDINATE>
7990
8091
Arguments:
81-
ENTITY GraphQL entity to locate. E.g. `User` or `User.id`
92+
COORDINATE Schema coordinate to locate. E.g. `User`, `User.name`,
93+
`Query.user(id:)`, `@deprecated`
8294
8395
Options:
8496
--tsconfig <TSCONFIG> Path to tsconfig.json. Defaults to auto-detecting based on the current working directory

src/Locate.ts

Lines changed: 96 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,115 @@
11
import {
22
GraphQLSchema,
33
Location,
4-
isObjectType,
5-
isInterfaceType,
6-
isInputObjectType,
4+
resolveSchemaCoordinate,
5+
type ResolvedSchemaElement,
76
} from "graphql";
87
import { Result, err, ok } from "./utils/Result.js";
98
import { nullThrows } from "./utils/helpers.js";
109

11-
type EntityName = {
12-
parent: string;
13-
field: string | null;
14-
};
15-
1610
/**
17-
* Given an entity name of the format `ParentType` or `ParentType.fieldName`,
18-
* locate the entity in the schema and return its location.
11+
* Given a schema coordinate string, locate the entity in the schema
12+
* and return its source location.
13+
*
14+
* Uses the Schema Coordinates spec:
15+
* https://spec.graphql.org/draft/#sec-Schema-Coordinates
16+
*
17+
* Supports all schema coordinate forms:
18+
* - `Type` — named type
19+
* - `Type.field` — field on object/interface type
20+
* - `Type.field(arg:)` — field argument
21+
* - `EnumType.VALUE` — enum value
22+
* - `InputType.field` — input field
23+
* - `@directive` — directive
24+
* - `@directive(arg:)` — directive argument
1925
*/
2026
export function locate(
2127
schema: GraphQLSchema,
22-
entityName: string,
28+
coordinate: string,
2329
): Result<Location, string> {
24-
const entityResult = parseEntityName(entityName);
25-
if (entityResult.kind === "ERROR") {
26-
return entityResult;
27-
}
28-
const entity = entityResult.value;
29-
const type = schema.getType(entity.parent);
30-
if (type == null) {
31-
return err(`Cannot locate type \`${entity.parent}\`.`);
32-
}
33-
if (entity.field == null) {
34-
if (type.astNode == null) {
35-
throw new Error(
36-
`Grats bug: Cannot find location of type \`${entity.parent}\`.`,
37-
);
38-
}
39-
return ok(nullThrows(type.astNode.name.loc));
40-
}
41-
42-
if (
43-
!(isObjectType(type) || isInterfaceType(type) || isInputObjectType(type))
44-
) {
30+
let resolved: ResolvedSchemaElement | undefined;
31+
try {
32+
resolved = resolveSchemaCoordinate(schema, coordinate);
33+
} catch (e: unknown) {
4534
return err(
46-
`Cannot locate field \`${entity.field}\` on type \`${entity.parent}\`. Only object types, interfaces, and input objects have fields.`,
35+
`Invalid schema coordinate: \`${coordinate}\`. ${e instanceof Error ? e.message : String(e)}`,
4736
);
4837
}
49-
50-
const field = type.getFields()[entity.field];
51-
if (field == null) {
52-
return err(
53-
`Cannot locate field \`${entity.field}\` on type \`${entity.parent}\`.`,
54-
);
55-
}
56-
57-
if (field.astNode == null) {
58-
throw new Error(
59-
`Grats bug: Cannot find location of field \`${entity.field}\` on type \`${entity.parent}\`.`,
60-
);
38+
if (resolved == null) {
39+
return err(`Could not resolve schema coordinate: \`${coordinate}\`.`);
6140
}
62-
return ok(nullThrows(field.astNode.name.loc));
63-
}
64-
65-
const ENTITY_NAME_REGEX = /^([A-Za-z0-9_]+)(?:\.([A-Za-z0-9_]+))?$/;
6641

67-
function parseEntityName(entityName: string): Result<EntityName, string> {
68-
const match = ENTITY_NAME_REGEX.exec(entityName);
69-
if (match == null) {
70-
return err(
71-
`Invalid entity name: \`${entityName}\`. Expected \`ParentType\` or \`ParentType.fieldName\`.`,
72-
);
42+
switch (resolved.kind) {
43+
case "NamedType": {
44+
const astNode = resolved.type.astNode;
45+
if (astNode == null) {
46+
throw new Error(
47+
`Grats bug: Cannot find location of type in coordinate \`${coordinate}\`.`,
48+
);
49+
}
50+
return ok(nullThrows(astNode.name.loc));
51+
}
52+
case "Field": {
53+
const astNode = resolved.field.astNode;
54+
if (astNode == null) {
55+
throw new Error(
56+
`Grats bug: Cannot find location of field in coordinate \`${coordinate}\`.`,
57+
);
58+
}
59+
return ok(nullThrows(astNode.name.loc));
60+
}
61+
case "InputField": {
62+
const astNode = resolved.inputField.astNode;
63+
if (astNode == null) {
64+
throw new Error(
65+
`Grats bug: Cannot find location of input field in coordinate \`${coordinate}\`.`,
66+
);
67+
}
68+
return ok(nullThrows(astNode.name.loc));
69+
}
70+
case "EnumValue": {
71+
const astNode = resolved.enumValue.astNode;
72+
if (astNode == null) {
73+
throw new Error(
74+
`Grats bug: Cannot find location of enum value in coordinate \`${coordinate}\`.`,
75+
);
76+
}
77+
return ok(nullThrows(astNode.name.loc));
78+
}
79+
case "FieldArgument": {
80+
const astNode = resolved.fieldArgument.astNode;
81+
if (astNode == null) {
82+
throw new Error(
83+
`Grats bug: Cannot find location of field argument in coordinate \`${coordinate}\`.`,
84+
);
85+
}
86+
return ok(nullThrows(astNode.name.loc));
87+
}
88+
case "Directive": {
89+
const astNode = resolved.directive.astNode;
90+
if (astNode == null) {
91+
throw new Error(
92+
`Grats bug: Cannot find location of directive in coordinate \`${coordinate}\`.`,
93+
);
94+
}
95+
return ok(nullThrows(astNode.name.loc));
96+
}
97+
case "DirectiveArgument": {
98+
const astNode = resolved.directiveArgument.astNode;
99+
if (astNode == null) {
100+
throw new Error(
101+
`Grats bug: Cannot find location of directive argument in coordinate \`${coordinate}\`.`,
102+
);
103+
}
104+
return ok(nullThrows(astNode.name.loc));
105+
}
106+
default: {
107+
// Exhaustive check — if new schema coordinate kinds are added,
108+
// TypeScript will catch this.
109+
const _exhaustive: never = resolved;
110+
throw new Error(
111+
`Grats bug: Unexpected schema coordinate kind: ${(resolved as { kind: string }).kind}`,
112+
);
113+
}
73114
}
74-
const parent = match[1];
75-
const field = match[2] || null;
76-
return ok({ parent, field });
77115
}

src/cli.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ program
7575

7676
program
7777
.command("locate")
78-
.argument("<ENTITY>", "GraphQL entity to locate. E.g. `User` or `User.id`")
78+
.argument(
79+
"<COORDINATE>",
80+
"Schema coordinate to locate. E.g. `User`, `User.name`, `Query.user(id:)`, `@deprecated`",
81+
)
7982
.option(
8083
"--tsconfig <TSCONFIG>",
8184
"Path to tsconfig.json. Defaults to auto-detecting based on the current working directory",
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Locate: Greeting.HELLO
2+
/** @gqlEnum */
3+
export enum Greeting {
4+
HELLO = "HELLO",
5+
GOODBYE = "GOODBYE",
6+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# locate/enumValue.invalid.ts
2+
3+
## Input
4+
5+
```ts title="locate/enumValue.invalid.ts"
6+
// Locate: Greeting.HELLO
7+
/** @gqlEnum */
8+
export enum Greeting {
9+
HELLO = "HELLO",
10+
GOODBYE = "GOODBYE",
11+
}
12+
```
13+
14+
## Output
15+
16+
### Error Report
17+
18+
```text
19+
src/tests/fixtures/locate/enumValue.invalid.ts:4:11 - error: Located here
20+
21+
4 HELLO = "HELLO",
22+
~~~~~~~
23+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Locate: Query.greeting(salutation:)
2+
/** @gqlType */
3+
type Query = unknown;
4+
5+
/** @gqlQueryField */
6+
export function greeting(salutation: string): string {
7+
return `${salutation}, world!`;
8+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# locate/fieldArgument.invalid.ts
2+
3+
## Input
4+
5+
```ts title="locate/fieldArgument.invalid.ts"
6+
// Locate: Query.greeting(salutation:)
7+
/** @gqlType */
8+
type Query = unknown;
9+
10+
/** @gqlQueryField */
11+
export function greeting(salutation: string): string {
12+
return `${salutation}, world!`;
13+
}
14+
```
15+
16+
## Output
17+
18+
### Error Report
19+
20+
```text
21+
src/tests/fixtures/locate/fieldArgument.invalid.ts:6:26 - error: Located here
22+
23+
6 export function greeting(salutation: string): string {
24+
~~~~~~~~~~
25+
```

src/tests/fixtures/locate/fieldOnScalar.invalid.ts.expected.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ export type Date = string;
1313
### Error Locating Type
1414

1515
```text
16-
Cannot locate field `name` on type `Date`. Only object types, interfaces, and input objects have fields.
16+
Invalid schema coordinate: `Date.name`. Expected "Date" to be an Enum, Input Object, Object or Interface type.
1717
```

src/tests/fixtures/locate/malformedEntitySyntax.invalid.ts.expected.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ type User = {
1616
### Error Locating Type
1717

1818
```text
19-
Invalid entity name: `User->name`. Expected `ParentType` or `ParentType.fieldName`.
19+
Invalid schema coordinate: `User->name`. Syntax Error: Invalid character: "-".
2020
```

src/tests/fixtures/locate/notFoundField.invalid.ts.expected.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ type User = {
1616
### Error Locating Type
1717

1818
```text
19-
Cannot locate field `not_a_field` on type `User`.
19+
Could not resolve schema coordinate: `User.not_a_field`.
2020
```

0 commit comments

Comments
 (0)