|
1 | 1 | import { |
2 | 2 | GraphQLSchema, |
3 | 3 | Location, |
4 | | - isObjectType, |
5 | | - isInterfaceType, |
6 | | - isInputObjectType, |
| 4 | + resolveSchemaCoordinate, |
| 5 | + type ResolvedSchemaElement, |
7 | 6 | } from "graphql"; |
8 | 7 | import { Result, err, ok } from "./utils/Result.js"; |
9 | 8 | import { nullThrows } from "./utils/helpers.js"; |
10 | 9 |
|
11 | | -type EntityName = { |
12 | | - parent: string; |
13 | | - field: string | null; |
14 | | -}; |
15 | | - |
16 | 10 | /** |
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 |
19 | 25 | */ |
20 | 26 | export function locate( |
21 | 27 | schema: GraphQLSchema, |
22 | | - entityName: string, |
| 28 | + coordinate: string, |
23 | 29 | ): 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) { |
45 | 34 | 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)}`, |
47 | 36 | ); |
48 | 37 | } |
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}\`.`); |
61 | 40 | } |
62 | | - return ok(nullThrows(field.astNode.name.loc)); |
63 | | -} |
64 | | - |
65 | | -const ENTITY_NAME_REGEX = /^([A-Za-z0-9_]+)(?:\.([A-Za-z0-9_]+))?$/; |
66 | 41 |
|
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 | + } |
73 | 114 | } |
74 | | - const parent = match[1]; |
75 | | - const field = match[2] || null; |
76 | | - return ok({ parent, field }); |
77 | 115 | } |
0 commit comments