Skip to content

Commit 8b2e2d4

Browse files
heiskrCopilot
andauthored
Article API: compact GraphQL reference output for LLM consumers (#60072)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ac2064f commit 8b2e2d4

3 files changed

Lines changed: 104 additions & 69 deletions

File tree

src/article-api/templates/graphql-reference.template.md

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44

55
{{ manualContent }}
66

7+
{% if connectionEdgeSummary %}
8+
## Connection and Edge types
9+
10+
Connection types with only the standard pagination fields (`edges`, `nodes`, `pageInfo`, `totalCount`) and Edge types with only `cursor` and `node` are summarized here. Connection and Edge types with additional fields are documented individually below.
11+
12+
{% for name in connectionEdgeSummary %}`{{ name }}`{% unless forloop.last %}, {% endunless %}{% endfor %}
13+
14+
{% endif %}
715
{% for item in items %}
816

917
## {{ item.name }}
@@ -17,15 +25,13 @@
1725
{% endif %}
1826

1927
{% if pageType == 'queries' %}
20-
**Type:** [{{ item.type }}]({{ item.href }})
28+
**Type:** {{ item.type }}
2129

2230
{% if item.args.size > 0 %}
2331

2432
### Arguments for `{{ item.name }}`
2533

26-
| Name | Type | Description |
27-
| --- | --- | --- |
28-
{% for arg in item.args %}| `{{ arg.name }}` | [`{{ arg.type }}`]({{ arg.href }}) | {{ arg.description }} |
34+
{% for arg in item.args %}* `{{ arg.name }}` ({{ arg.type }}): {{ arg.description }}
2935
{% endfor %}
3036
{% endif %}
3137

@@ -34,38 +40,31 @@
3440

3541
### Input fields for `{{ item.name }}`
3642

37-
| Name | Type | Description |
38-
| --- | --- | --- |
39-
{% for field in item.inputFields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %} |
43+
{% for field in item.inputFields %}* `{{ field.name }}` ({{ field.type }}): {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}
4044
{% endfor %}
4145
{% endif %}
4246

4347
{% if item.returnFields.size > 0 %}
4448

4549
### Return fields for `{{ item.name }}`
4650

47-
| Name | Type | Description |
48-
| --- | --- | --- |
49-
{% for field in item.returnFields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %} |
51+
{% for field in item.returnFields %}* `{{ field.name }}` ({{ field.type }}): {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %}
5052
{% endfor %}
5153
{% endif %}
5254

5355
{% elsif pageType == 'objects' %}
5456
{% if item.implements.size > 0 %}
57+
**Implements:** {% for impl in item.implements %}{{ impl.name }}{% unless forloop.last %}, {% endunless %}{% endfor %}
5558

56-
### Implements
57-
58-
{% for impl in item.implements %}- [`{{ impl.name }}`]({{ impl.href }})
59-
{% endfor %}
6059
{% endif %}
6160

6261
{% if item.fields.size > 0 %}
6362

6463
### Fields for `{{ item.name }}`
6564

66-
| Name | Type | Description |
67-
| --- | --- | --- |
68-
{% for field in item.fields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %}{% if field.arguments.size > 0 %}<br><br>**Arguments:**<br>{% for arg in field.arguments %}- `{{ arg.name }}` ([`{{ arg.type.name }}`]({{ arg.type.href }})): {{ arg.description }}{% if arg.defaultValue %} Default: `{{ arg.defaultValue }}`.{% endif %}<br>{% endfor %}{% endif %} |
65+
{% for field in item.fields %}* `{{ field.name }}` ({{ field.type }}): {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %}{% if field.arguments.size > 0 %}
66+
{% for arg in field.arguments %} * `{{ arg.name }}` ({{ arg.type.name }}): {{ arg.description }}{% if arg.defaultValue %} Default: `{{ arg.defaultValue }}`.{% endif %}
67+
{% endfor %}{% endif %}
6968
{% endfor %}
7069
{% endif %}
7170

@@ -74,9 +73,9 @@
7473

7574
### Fields for `{{ item.name }}`
7675

77-
| Name | Type | Description |
78-
| --- | --- | --- |
79-
{% for field in item.fields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %}{% if field.arguments.size > 0 %}<br><br>**Arguments:**<br>{% for arg in field.arguments %}- `{{ arg.name }}` ([`{{ arg.type.name }}`]({{ arg.type.href }})): {{ arg.description }}{% if arg.defaultValue %} Default: `{{ arg.defaultValue }}`.{% endif %}<br>{% endfor %}{% endif %} |
76+
{% for field in item.fields %}* `{{ field.name }}` ({{ field.type }}): {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %}{% if field.arguments.size > 0 %}
77+
{% for arg in field.arguments %} * `{{ arg.name }}` ({{ arg.type.name }}): {{ arg.description }}{% if arg.defaultValue %} Default: `{{ arg.defaultValue }}`.{% endif %}
78+
{% endfor %}{% endif %}
8079
{% endfor %}
8180
{% endif %}
8281

@@ -85,10 +84,7 @@
8584

8685
### Values for `{{ item.name }}`
8786

88-
{% for value in item.values %}**`{{ value.name }}`**
89-
90-
{{ value.description }}
91-
87+
{% for value in item.values %}* `{{ value.name }}`: {{ value.description }}
9288
{% endfor %}
9389
{% endif %}
9490

@@ -97,7 +93,7 @@
9793

9894
### Possible types for `{{ item.name }}`
9995

100-
{% for type in item.possibleTypes %}- [`{{ type.name }}`]({{ type.href }})
96+
{% for type in item.possibleTypes %}* {{ type.name }}
10197
{% endfor %}
10298
{% endif %}
10399

@@ -106,9 +102,7 @@
106102

107103
### Input fields for `{{ item.name }}`
108104

109-
| Name | Type | Description |
110-
| --- | --- | --- |
111-
{% for field in item.inputFields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %} |
105+
{% for field in item.inputFields %}* `{{ field.name }}` ({{ field.type }}): {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %}
112106
{% endfor %}
113107
{% endif %}
114108

src/article-api/tests/graphql-transformer.ts

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,19 @@ describe('GraphQL transformer', { timeout: 10000 }, () => {
5555
// Check for query description
5656
expect(res.body).toContain('Lookup a given repository by the owner and repository name.')
5757

58-
// Check for type link
59-
expect(res.body).toContain('**Type:** [Repository](/en/graphql/reference/objects#repository)')
58+
// Check for type (without link)
59+
expect(res.body).toContain('**Type:** Repository')
6060
})
6161

62-
test('query arguments are listed in table format', async () => {
62+
test('query arguments are listed in bullet format', async () => {
6363
const res = await getCached('/en/graphql/reference/queries')
6464
expect(res.statusCode).toBe(200)
6565

66-
// Check for arguments table for codeOfConduct query
66+
// Check for arguments section for codeOfConduct query
6767
expect(res.body).toContain('### Arguments for `codeOfConduct`')
68-
expect(res.body).toMatch(/\|\s*Name\s*\|\s*Type\s*\|\s*Description\s*\|/)
69-
expect(res.body).toMatch(/\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|/)
7068

71-
// Check for specific arguments
72-
expect(res.body).toMatch(/\|\s*`key`\s*\|/)
73-
expect(res.body).toContain('[`String!`](/en/graphql/reference/scalars#string)')
69+
// Check for specific arguments in bullet format
70+
expect(res.body).toContain('`key` (String!)')
7471
expect(res.body).toContain("The code of conduct's key.")
7572
})
7673

@@ -84,13 +81,13 @@ describe('GraphQL transformer', { timeout: 10000 }, () => {
8481
// Check for mutation description
8582
expect(res.body).toContain('Create a new repository.')
8683

87-
// Check for input fields table
84+
// Check for input fields in bullet format
8885
expect(res.body).toContain('### Input fields for `createRepository`')
89-
expect(res.body).toContain('| `input` |')
86+
expect(res.body).toContain('`input`')
9087

91-
// Check for return fields table
88+
// Check for return fields in bullet format
9289
expect(res.body).toContain('### Return fields for `createRepository`')
93-
expect(res.body).toMatch(/\|\s*`repository`\s*\|/)
90+
expect(res.body).toContain('`repository`')
9491
expect(res.body).toContain('The new repository.')
9592
})
9693

@@ -101,30 +98,42 @@ describe('GraphQL transformer', { timeout: 10000 }, () => {
10198
// Check for object heading - AddedToMergeQueueEvent has implements
10299
expect(res.body).toContain('## AddedToMergeQueueEvent')
103100

104-
// Check for implements section
105-
expect(res.body).toContain('### Implements')
106-
expect(res.body).toMatch(/[*-]\s*\[`Node`\]\(\/.*graphql\/reference\/interfaces#node\)/)
101+
// Check for implements inline
102+
expect(res.body).toContain('**Implements:** Node')
107103

108-
// Check for fields table
104+
// Check for fields in bullet format
109105
expect(res.body).toContain('### Fields for `AddedToMergeQueueEvent`')
110-
expect(res.body).toMatch(/\|\s*`id`\s*\|/)
111-
expect(res.body).toMatch(/\|\s*`actor`\s*\|/)
112-
expect(res.body).toMatch(/\|\s*`createdAt`\s*\|/)
106+
expect(res.body).toContain('`id`')
107+
expect(res.body).toContain('`actor`')
108+
expect(res.body).toContain('`createdAt`')
113109
})
114110

115-
test('objects page shows field arguments inline', async () => {
111+
test('objects page shows field arguments as nested bullets', async () => {
116112
const res = await getCached('/en/graphql/reference/objects')
117113
expect(res.statusCode).toBe(200)
118114

119115
// Check for User object with repositories field that has arguments
120116
expect(res.body).toContain('## User')
121-
expect(res.body).toContain('| `repositories` |')
117+
expect(res.body).toContain('`repositories`')
122118

123-
// Check for inline arguments formatting
124-
expect(res.body).toContain('**Arguments:**')
125-
expect(res.body).toContain('- `first`')
119+
// Check for nested argument bullets
120+
expect(res.body).toContain('`first`')
126121
expect(res.body).toContain('Returns the first n elements from the list.')
127-
expect(res.body).toContain('- `orderBy`')
122+
expect(res.body).toContain('`orderBy`')
123+
})
124+
125+
test('objects page collapses boilerplate Connection and Edge types', async () => {
126+
const res = await getCached('/en/graphql/reference/objects')
127+
expect(res.statusCode).toBe(200)
128+
129+
// Check for Connection/Edge summary section
130+
expect(res.body).toContain('## Connection and Edge types')
131+
expect(res.body).toContain('standard pagination fields')
132+
133+
// Boilerplate Connection/Edge types should be in the summary, not as H2 sections
134+
// ActorConnection has only standard fields (edges, nodes, pageInfo, totalCount)
135+
expect(res.body).toContain('`ActorConnection`')
136+
expect(res.body).not.toMatch(/^## ActorConnection$/m)
128137
})
129138

130139
test('interfaces page renders correctly', async () => {
@@ -137,9 +146,9 @@ describe('GraphQL transformer', { timeout: 10000 }, () => {
137146
// Check for interface description
138147
expect(res.body).toContain('An object with an ID.')
139148

140-
// Check for fields table
149+
// Check for fields in bullet format
141150
expect(res.body).toContain('### Fields for `Node`')
142-
expect(res.body).toContain('| `id` |')
151+
expect(res.body).toContain('`id`')
143152
expect(res.body).toContain('ID of the object.')
144153
})
145154

@@ -153,13 +162,13 @@ describe('GraphQL transformer', { timeout: 10000 }, () => {
153162
// Check for enum description
154163
expect(res.body).toContain("The repository's visibility level.")
155164

156-
// Check for values section
165+
// Check for values in bullet format
157166
expect(res.body).toContain('### Values for `RepositoryVisibility`')
158-
expect(res.body).toContain('**`PUBLIC`**')
167+
expect(res.body).toContain('`PUBLIC`')
159168
expect(res.body).toContain('The repository is visible to everyone.')
160-
expect(res.body).toContain('**`PRIVATE`**')
169+
expect(res.body).toContain('`PRIVATE`')
161170
expect(res.body).toContain('The repository is visible only to those with explicit access.')
162-
expect(res.body).toContain('**`INTERNAL`**')
171+
expect(res.body).toContain('`INTERNAL`')
163172
})
164173

165174
test('unions page renders with possible types', async () => {
@@ -172,13 +181,11 @@ describe('GraphQL transformer', { timeout: 10000 }, () => {
172181
// Check for union description
173182
expect(res.body).toContain('The results of a search.')
174183

175-
// Check for possible types
184+
// Check for possible types in bullet format (without links)
176185
expect(res.body).toContain('### Possible types for `SearchResultItem`')
177-
expect(res.body).toMatch(/[*-]\s*\[`Bot`\]\(\/.*graphql\/reference\/objects#bot\)/)
178-
expect(res.body).toMatch(
179-
/[*-]\s*\[`PullRequest`\]\(\/.*graphql\/reference\/objects#pullrequest\)/,
180-
)
181-
expect(res.body).toMatch(/[*-]\s*\[`User`\]\(\/.*graphql\/reference\/objects#user\)/)
186+
expect(res.body).toMatch(/\*\s*Bot/)
187+
expect(res.body).toMatch(/\*\s*PullRequest/)
188+
expect(res.body).toMatch(/\*\s*User/)
182189
})
183190

184191
test('input-objects page renders correctly', async () => {
@@ -191,9 +198,9 @@ describe('GraphQL transformer', { timeout: 10000 }, () => {
191198
// Check for input object description
192199
expect(res.body).toContain('Autogenerated input type of CreateRepository.')
193200

194-
// Check for input fields table
201+
// Check for input fields in bullet format
195202
expect(res.body).toContain('### Input fields for `AbortQueuedMigrationsInput`')
196-
expect(res.body).toMatch(/\|\s*`ownerId`\s*\|/)
203+
expect(res.body).toContain('`ownerId`')
197204
expect(res.body).toContain('The ID of the organization that is running the migrations.')
198205
})
199206

@@ -314,7 +321,6 @@ describe('GraphQL transformer', { timeout: 10000 }, () => {
314321

315322
// Check that AUTOTITLE has been resolved
316323
expect(res.body).toMatch(/(Forming calls with GraphQL|Hello World)/)
317-
expect(res.body).toContain('(/en/get-started/start-your-journey/hello-world)')
318324

319325
// Make sure the raw AUTOTITLE tag is not present
320326
expect(res.body).not.toContain('[AUTOTITLE]')

src/article-api/transformers/graphql-reference-transformer.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,47 @@ export class GraphQLReferenceTransformer implements PageTransformer {
9797
break
9898
}
9999

100+
// For objects pages, collapse Connection/Edge types that have only standard
101+
// boilerplate fields into a summary. Types with additional fields are kept.
102+
let connectionEdgeSummary: string[] | null = null
103+
if (schemaKey === 'objects') {
104+
const BOILERPLATE_CONNECTION_FIELDS = new Set(['edges', 'nodes', 'pageInfo', 'totalCount'])
105+
const BOILERPLATE_EDGE_FIELDS = new Set(['cursor', 'node'])
106+
const connEdgeNames: string[] = []
107+
const filteredItems: Array<Record<string, unknown>> = []
108+
for (const item of preparedItems) {
109+
const name = item.name as string
110+
const fields = item.fields as Array<Record<string, unknown>> | undefined
111+
const fieldNames = new Set((fields || []).map((f) => f.name as string))
112+
113+
const isBoilerplateConnection =
114+
name.endsWith('Connection') &&
115+
fieldNames.size === BOILERPLATE_CONNECTION_FIELDS.size &&
116+
[...fieldNames].every((f) => BOILERPLATE_CONNECTION_FIELDS.has(f))
117+
const isBoilerplateEdge =
118+
name.endsWith('Edge') &&
119+
fieldNames.size === BOILERPLATE_EDGE_FIELDS.size &&
120+
[...fieldNames].every((f) => BOILERPLATE_EDGE_FIELDS.has(f))
121+
122+
if (isBoilerplateConnection || isBoilerplateEdge) {
123+
connEdgeNames.push(name)
124+
} else {
125+
filteredItems.push(item)
126+
}
127+
}
128+
if (connEdgeNames.length > 0) {
129+
connectionEdgeSummary = connEdgeNames.sort()
130+
preparedItems = filteredItems
131+
}
132+
}
133+
100134
const templateData: Record<string, unknown> = {
101135
pageTitle: page.title,
102136
pageIntro: intro,
103137
manualContent,
104138
items: preparedItems,
105139
pageType: schemaKey,
140+
connectionEdgeSummary,
106141
}
107142

108143
const templateContent = loadTemplate(this.templateName)

0 commit comments

Comments
 (0)