Add GraphQL transformer for Article API (#58719)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -23,7 +23,12 @@ The `/api/article` endpoints return information about a page by `pathname`.
|
||||
|
||||
### Autogenerated Content Transformers
|
||||
|
||||
For autogenerated pages (REST, landing pages, audit logs, webhooks, GraphQL, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture:
|
||||
For autogenerated pages (REST, GraphQL, webhooks, landing pages, audit logs, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture:
|
||||
|
||||
#### Current Transformers
|
||||
|
||||
- **REST Transformer** (`rest-transformer.ts`) - Converts REST API operations into markdown, including endpoints, parameters, status codes, and code examples
|
||||
- **GraphQL Transformer** (`graphql-transformer.ts`) - Converts GraphQL schema documentation into markdown, including queries, mutations, objects, interfaces, enums, unions, input objects, scalars, changelog, and breaking changes
|
||||
|
||||
To add a new transformer for other autogenerated content types:
|
||||
1. Create a new transformer file implementing the `PageTransformer` interface
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# {{ pageTitle }}
|
||||
|
||||
{{ pageIntro }}
|
||||
|
||||
{{ manualContent }}
|
||||
|
||||
{% for change in breakingChangesByDate %}
|
||||
|
||||
## {{ change.heading }}
|
||||
|
||||
{% for item in change.items %}
|
||||
|
||||
- {% if item.criticality == 'breaking' %}**Breaking**{% else %}**Dangerous**{% endif %} A change will be made to `{{ item.location }}`.
|
||||
|
||||
**Description:** {{ item.description }}
|
||||
|
||||
**Reason:** {{ item.reason }}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
38
src/article-api/templates/graphql-changelog.template.md
Normal file
38
src/article-api/templates/graphql-changelog.template.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# {{ pageTitle }}
|
||||
|
||||
{{ pageIntro }}
|
||||
|
||||
{{ manualContent }}
|
||||
|
||||
{% for item in changelogItems %}
|
||||
|
||||
## Schema changes for {{ item.date }}
|
||||
|
||||
{% for schemaChange in item.schemaChanges %}
|
||||
|
||||
### {{ schemaChange.title }}
|
||||
|
||||
{% for change in schemaChange.changes %}- {{ change }}
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% for previewChange in item.previewChanges %}
|
||||
|
||||
### {{ previewChange.title }}
|
||||
|
||||
{% for change in previewChange.changes %}- {{ change }}
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% for upcomingChange in item.upcomingChanges %}
|
||||
|
||||
### {{ upcomingChange.title }}
|
||||
|
||||
{% for change in upcomingChange.changes %}- {{ change }}
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
9
src/article-api/templates/graphql-index.template.md
Normal file
9
src/article-api/templates/graphql-index.template.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# {{ pageTitle }}
|
||||
|
||||
{{ pageIntro }}
|
||||
|
||||
{{ manualContent }}
|
||||
|
||||
## Reference pages
|
||||
|
||||
{{ childrenLinks }}
|
||||
120
src/article-api/templates/graphql-reference.template.md
Normal file
120
src/article-api/templates/graphql-reference.template.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# {{ pageTitle }}
|
||||
|
||||
{{ pageIntro }}
|
||||
|
||||
{{ manualContent }}
|
||||
|
||||
{% for item in items %}
|
||||
|
||||
## {{ item.name }}
|
||||
|
||||
{{ item.description }}
|
||||
|
||||
{% if item.isDeprecated %}
|
||||
|
||||
> [!WARNING]
|
||||
> **Deprecation notice:** {{ item.deprecationReason }}
|
||||
{% endif %}
|
||||
|
||||
{% if pageType == 'queries' %}
|
||||
**Type:** [{{ item.type }}]({{ item.href }})
|
||||
|
||||
{% if item.args.size > 0 %}
|
||||
|
||||
### Arguments for `{{ item.name }}`
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
{% for arg in item.args %}| `{{ arg.name }}` | [`{{ arg.type }}`]({{ arg.href }}) | {{ arg.description }} |
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% elsif pageType == 'mutations' %}
|
||||
{% if item.inputFields.size > 0 %}
|
||||
|
||||
### Input fields for `{{ item.name }}`
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
{% for field in item.inputFields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %} |
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if item.returnFields.size > 0 %}
|
||||
|
||||
### Return fields for `{{ item.name }}`
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
{% 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 %} |
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% elsif pageType == 'objects' %}
|
||||
{% if item.implements.size > 0 %}
|
||||
|
||||
### Implements
|
||||
|
||||
{% for impl in item.implements %}- [`{{ impl.name }}`]({{ impl.href }})
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if item.fields.size > 0 %}
|
||||
|
||||
### Fields for `{{ item.name }}`
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
{% 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 %} |
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% elsif pageType == 'interfaces' %}
|
||||
{% if item.fields.size > 0 %}
|
||||
|
||||
### Fields for `{{ item.name }}`
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
{% 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 %} |
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% elsif pageType == 'enums' %}
|
||||
{% if item.values.size > 0 %}
|
||||
|
||||
### Values for `{{ item.name }}`
|
||||
|
||||
{% for value in item.values %}**`{{ value.name }}`**
|
||||
|
||||
{{ value.description }}
|
||||
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% elsif pageType == 'unions' %}
|
||||
{% if item.possibleTypes.size > 0 %}
|
||||
|
||||
### Possible types for `{{ item.name }}`
|
||||
|
||||
{% for type in item.possibleTypes %}- [`{{ type.name }}`]({{ type.href }})
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% elsif pageType == 'inputObjects' %}
|
||||
{% if item.inputFields.size > 0 %}
|
||||
|
||||
### Input fields for `{{ item.name }}`
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
{% 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 %} |
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% elsif pageType == 'scalars' %}
|
||||
{%- comment -%}Scalars typically just have name and description{%- endcomment -%}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
370
src/article-api/tests/graphql-transformer.ts
Normal file
370
src/article-api/tests/graphql-transformer.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import { beforeAll, describe, expect, test } from 'vitest'
|
||||
|
||||
import { get } from '@/tests/helpers/e2etest'
|
||||
|
||||
const makeURL = (pathname: string): string => {
|
||||
const params = new URLSearchParams({ pathname })
|
||||
return `/api/article/body?${params}`
|
||||
}
|
||||
|
||||
describe('GraphQL transformer', { timeout: 10000 }, () => {
|
||||
// Cache expensive responses to avoid duplicate requests
|
||||
const responseCache = new Map<string, Awaited<ReturnType<typeof get>>>()
|
||||
|
||||
const getCached = async (url: string) => {
|
||||
if (!responseCache.has(url)) {
|
||||
responseCache.set(url, await get(makeURL(url)))
|
||||
}
|
||||
return responseCache.get(url)!
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
if (!process.env.ROOT) {
|
||||
console.warn(
|
||||
'WARNING: The GraphQL transformer tests require the ROOT environment variable to be set to the fixture root',
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('Reference pages', () => {
|
||||
test('queries page renders with markdown structure', async () => {
|
||||
const res = await getCached('/en/graphql/reference/queries')
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.headers['content-type']).toContain('text/markdown')
|
||||
|
||||
// Check for the main heading
|
||||
expect(res.body).toContain('# Queries')
|
||||
|
||||
// Check for intro
|
||||
expect(res.body).toContain(
|
||||
'The query type defines GraphQL operations that retrieve data from the server.',
|
||||
)
|
||||
|
||||
// Check for manual content section
|
||||
expect(res.body).toContain('## About queries')
|
||||
expect(res.body).toContain('Every GraphQL schema has a root type')
|
||||
})
|
||||
|
||||
test('queries are formatted correctly', async () => {
|
||||
const res = await getCached('/en/graphql/reference/queries')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for query heading
|
||||
expect(res.body).toContain('## repository')
|
||||
|
||||
// Check for query description
|
||||
expect(res.body).toContain('Lookup a given repository by the owner and repository name.')
|
||||
|
||||
// Check for type link
|
||||
expect(res.body).toContain('**Type:** [Repository](/en/graphql/reference/objects#repository)')
|
||||
})
|
||||
|
||||
test('query arguments are listed in table format', async () => {
|
||||
const res = await getCached('/en/graphql/reference/queries')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for arguments table for codeOfConduct query
|
||||
expect(res.body).toContain('### Arguments for `codeOfConduct`')
|
||||
expect(res.body).toMatch(/\|\s*Name\s*\|\s*Type\s*\|\s*Description\s*\|/)
|
||||
expect(res.body).toMatch(/\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|/)
|
||||
|
||||
// Check for specific arguments
|
||||
expect(res.body).toMatch(/\|\s*`key`\s*\|/)
|
||||
expect(res.body).toContain('[`String!`](/en/graphql/reference/scalars#string)')
|
||||
expect(res.body).toContain("The code of conduct's key.")
|
||||
})
|
||||
|
||||
test('mutations page renders correctly', async () => {
|
||||
const res = await getCached('/en/graphql/reference/mutations')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for mutation heading
|
||||
expect(res.body).toContain('## createRepository')
|
||||
|
||||
// Check for mutation description
|
||||
expect(res.body).toContain('Create a new repository.')
|
||||
|
||||
// Check for input fields table
|
||||
expect(res.body).toContain('### Input fields for `createRepository`')
|
||||
expect(res.body).toContain('| `input` |')
|
||||
|
||||
// Check for return fields table
|
||||
expect(res.body).toContain('### Return fields for `createRepository`')
|
||||
expect(res.body).toMatch(/\|\s*`repository`\s*\|/)
|
||||
expect(res.body).toContain('The new repository.')
|
||||
})
|
||||
|
||||
test('objects page renders with implements and fields', async () => {
|
||||
const res = await getCached('/en/graphql/reference/objects')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for object heading - AddedToMergeQueueEvent has implements
|
||||
expect(res.body).toContain('## AddedToMergeQueueEvent')
|
||||
|
||||
// Check for implements section
|
||||
expect(res.body).toContain('### Implements')
|
||||
expect(res.body).toMatch(/[*-]\s*\[`Node`\]\(\/.*graphql\/reference\/interfaces#node\)/)
|
||||
|
||||
// Check for fields table
|
||||
expect(res.body).toContain('### Fields for `AddedToMergeQueueEvent`')
|
||||
expect(res.body).toMatch(/\|\s*`id`\s*\|/)
|
||||
expect(res.body).toMatch(/\|\s*`actor`\s*\|/)
|
||||
expect(res.body).toMatch(/\|\s*`createdAt`\s*\|/)
|
||||
})
|
||||
|
||||
test('objects page shows field arguments inline', async () => {
|
||||
const res = await getCached('/en/graphql/reference/objects')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for User object with repositories field that has arguments
|
||||
expect(res.body).toContain('## User')
|
||||
expect(res.body).toContain('| `repositories` |')
|
||||
|
||||
// Check for inline arguments formatting
|
||||
expect(res.body).toContain('**Arguments:**')
|
||||
expect(res.body).toContain('- `first`')
|
||||
expect(res.body).toContain('Returns the first n elements from the list.')
|
||||
expect(res.body).toContain('- `orderBy`')
|
||||
})
|
||||
|
||||
test('interfaces page renders correctly', async () => {
|
||||
const res = await getCached('/en/graphql/reference/interfaces')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for interface heading
|
||||
expect(res.body).toContain('## Node')
|
||||
|
||||
// Check for interface description
|
||||
expect(res.body).toContain('An object with an ID.')
|
||||
|
||||
// Check for fields table
|
||||
expect(res.body).toContain('### Fields for `Node`')
|
||||
expect(res.body).toContain('| `id` |')
|
||||
expect(res.body).toContain('ID of the object.')
|
||||
})
|
||||
|
||||
test('enums page renders with values', async () => {
|
||||
const res = await getCached('/en/graphql/reference/enums')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for enum heading
|
||||
expect(res.body).toContain('## RepositoryVisibility')
|
||||
|
||||
// Check for enum description
|
||||
expect(res.body).toContain("The repository's visibility level.")
|
||||
|
||||
// Check for values section
|
||||
expect(res.body).toContain('### Values for `RepositoryVisibility`')
|
||||
expect(res.body).toContain('**`PUBLIC`**')
|
||||
expect(res.body).toContain('The repository is visible to everyone.')
|
||||
expect(res.body).toContain('**`PRIVATE`**')
|
||||
expect(res.body).toContain('The repository is visible only to those with explicit access.')
|
||||
expect(res.body).toContain('**`INTERNAL`**')
|
||||
})
|
||||
|
||||
test('unions page renders with possible types', async () => {
|
||||
const res = await getCached('/en/graphql/reference/unions')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for union heading
|
||||
expect(res.body).toContain('## SearchResultItem')
|
||||
|
||||
// Check for union description
|
||||
expect(res.body).toContain('The results of a search.')
|
||||
|
||||
// Check for possible types
|
||||
expect(res.body).toContain('### Possible types for `SearchResultItem`')
|
||||
expect(res.body).toMatch(/[*-]\s*\[`Bot`\]\(\/.*graphql\/reference\/objects#bot\)/)
|
||||
expect(res.body).toMatch(
|
||||
/[*-]\s*\[`PullRequest`\]\(\/.*graphql\/reference\/objects#pullrequest\)/,
|
||||
)
|
||||
expect(res.body).toMatch(/[*-]\s*\[`User`\]\(\/.*graphql\/reference\/objects#user\)/)
|
||||
})
|
||||
|
||||
test('input-objects page renders correctly', async () => {
|
||||
const res = await getCached('/en/graphql/reference/input-objects')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for input object heading
|
||||
expect(res.body).toContain('## AbortQueuedMigrationsInput')
|
||||
|
||||
// Check for input object description
|
||||
expect(res.body).toContain('Autogenerated input type of CreateRepository.')
|
||||
|
||||
// Check for input fields table
|
||||
expect(res.body).toContain('### Input fields for `AbortQueuedMigrationsInput`')
|
||||
expect(res.body).toMatch(/\|\s*`ownerId`\s*\|/)
|
||||
expect(res.body).toContain('The ID of the organization that is running the migrations.')
|
||||
})
|
||||
|
||||
test('scalars page renders correctly', async () => {
|
||||
const res = await getCached('/en/graphql/reference/scalars')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for scalar heading
|
||||
expect(res.body).toContain('## Boolean')
|
||||
|
||||
// Check for scalar description
|
||||
expect(res.body).toContain('Represents true or false values.')
|
||||
|
||||
// Check for other scalars
|
||||
expect(res.body).toContain('## String')
|
||||
expect(res.body).toContain('## ID')
|
||||
expect(res.body).toContain('## Int')
|
||||
})
|
||||
|
||||
test('reference index page renders', async () => {
|
||||
const res = await getCached('/en/graphql/reference')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for main heading
|
||||
expect(res.body).toContain('# Reference')
|
||||
|
||||
// Check for intro with liquid variable rendered
|
||||
expect(res.body).toMatch(/(GitHub|HubGit) GraphQL API schema/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Overview pages', () => {
|
||||
test('changelog page renders with changes', async () => {
|
||||
const res = await getCached('/en/graphql/overview/changelog')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for main heading
|
||||
expect(res.body).toContain('# Changelog')
|
||||
|
||||
// Check for intro
|
||||
expect(res.body).toContain(
|
||||
'The GraphQL schema changelog is a list of recent and upcoming changes',
|
||||
)
|
||||
|
||||
// Check for manual content
|
||||
expect(res.body).toContain(
|
||||
'Breaking changes include changes that will break existing queries',
|
||||
)
|
||||
|
||||
// Check for date-based changelog sections
|
||||
expect(res.body).toContain('## Schema changes for 2025-11-30')
|
||||
|
||||
// Check for change items
|
||||
expect(res.body).toContain('### The GraphQL schema includes these changes:')
|
||||
expect(res.body).toContain('Type SuggestedReviewerActor was added')
|
||||
})
|
||||
|
||||
test('changelog removes HTML tags from changes', async () => {
|
||||
const res = await getCached('/en/graphql/overview/changelog')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check that HTML tags are removed
|
||||
expect(res.body).toContain('Field suggestedReviewerActors was added')
|
||||
expect(res.body).not.toContain('<code>')
|
||||
expect(res.body).not.toContain('</code>')
|
||||
expect(res.body).not.toContain('<p>')
|
||||
expect(res.body).not.toContain('</p>')
|
||||
})
|
||||
|
||||
test('breaking changes page renders with scheduled changes', async () => {
|
||||
const res = await getCached('/en/graphql/overview/breaking-changes')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for main heading
|
||||
expect(res.body).toContain('# Breaking changes')
|
||||
|
||||
// Check for intro
|
||||
expect(res.body).toContain('Learn about recent and upcoming breaking changes')
|
||||
|
||||
// Check for manual content
|
||||
expect(res.body).toContain('## About breaking changes')
|
||||
expect(res.body).toContain('Breaking:** Changes that will break existing queries')
|
||||
|
||||
// Check for date-based sections
|
||||
expect(res.body).toContain('## Changes scheduled for 2025-04-01')
|
||||
expect(res.body).toContain('## Changes scheduled for 2026-04-01')
|
||||
})
|
||||
|
||||
test('breaking changes shows criticality levels', async () => {
|
||||
const res = await getCached('/en/graphql/overview/breaking-changes')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for breaking criticality
|
||||
expect(res.body).toMatch(/\*\*Breaking\*\*\s+A change will be made to `\w+\.\w+`\./)
|
||||
expect(res.body).toMatch(/\*\*Description:\*\*.*will be removed/)
|
||||
expect(res.body).toMatch(/\*\*Reason:\*\*/)
|
||||
})
|
||||
|
||||
test('breaking changes removes HTML tags', async () => {
|
||||
const res = await getCached('/en/graphql/overview/breaking-changes')
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.body).toContain('scheduled for')
|
||||
|
||||
// Check that HTML tags are removed from descriptions
|
||||
expect(res.body).not.toContain('<p>')
|
||||
expect(res.body).not.toContain('</p>')
|
||||
expect(res.body).not.toContain('<code>')
|
||||
expect(res.body).not.toContain('</code>')
|
||||
expect(res.body).not.toContain('<p>')
|
||||
expect(res.body).not.toContain('</p>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Liquid tags', () => {
|
||||
test('AUTOTITLE links are resolved in manual content', async () => {
|
||||
const res = await getCached('/en/graphql/reference/queries')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check that AUTOTITLE has been resolved
|
||||
expect(res.body).toMatch(/(Forming calls with GraphQL|Hello World)/)
|
||||
expect(res.body).toContain('(/en/get-started/start-your-journey/hello-world)')
|
||||
|
||||
// Make sure the raw AUTOTITLE tag is not present
|
||||
expect(res.body).not.toContain('[AUTOTITLE]')
|
||||
})
|
||||
|
||||
test('Liquid variables are rendered in intro', async () => {
|
||||
const res = await getCached('/en/graphql/reference')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Liquid variables should be rendered
|
||||
expect(res.body).toMatch(/(GitHub|HubGit) GraphQL API schema/)
|
||||
expect(res.body).not.toContain('{% data variables.product.prodname_dotcom %}')
|
||||
})
|
||||
|
||||
test('Liquid variables are rendered in breaking changes', async () => {
|
||||
const res = await getCached('/en/graphql/overview/breaking-changes')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check that liquid variables in intro are rendered
|
||||
expect(res.body).toMatch(/(GitHub|HubGit) GraphQL API/)
|
||||
expect(res.body).not.toContain('{% data variables.product.prodname_dotcom %}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multiple items', () => {
|
||||
test('multiple queries are all rendered', async () => {
|
||||
const res = await getCached('/en/graphql/reference/queries')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for multiple query headings
|
||||
expect(res.body).toContain('## repository')
|
||||
expect(res.body).toContain('## viewer')
|
||||
})
|
||||
|
||||
test('multiple objects are all rendered', async () => {
|
||||
const res = await getCached('/en/graphql/reference/objects')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for multiple object headings
|
||||
expect(res.body).toContain('## Repository')
|
||||
expect(res.body).toContain('## User')
|
||||
})
|
||||
|
||||
test('multiple enums are all rendered', async () => {
|
||||
const res = await getCached('/en/graphql/reference/enums')
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
||||
// Check for multiple enum headings
|
||||
expect(res.body).toContain('## RepositoryVisibility')
|
||||
expect(res.body).toContain('## OrderDirection')
|
||||
})
|
||||
})
|
||||
})
|
||||
478
src/article-api/transformers/graphql-transformer.ts
Normal file
478
src/article-api/transformers/graphql-transformer.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
import type { Context, Page } from '@/types'
|
||||
import type { PageTransformer } from './types'
|
||||
import type {
|
||||
QueryT,
|
||||
MutationT,
|
||||
ObjectT,
|
||||
InterfaceT,
|
||||
EnumT,
|
||||
UnionT,
|
||||
InputObjectT,
|
||||
ScalarT,
|
||||
ChangelogItemT,
|
||||
BreakingChangesT,
|
||||
FieldT,
|
||||
} from '@/graphql/components/types'
|
||||
import { renderContent } from '@/content-render/index'
|
||||
import matter from '@gr2m/gray-matter'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { fastTextOnly } from '@/content-render/unified/text-only'
|
||||
import GithubSlugger from 'github-slugger'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
/**
|
||||
* Transformer for GraphQL pages
|
||||
* Converts GraphQL schema data into markdown format using Liquid templates
|
||||
*/
|
||||
export class GraphQLTransformer implements PageTransformer {
|
||||
canTransform(page: Page): boolean {
|
||||
return page.autogenerated === 'graphql'
|
||||
}
|
||||
|
||||
async transform(page: Page, pathname: string, context: Context): Promise<string> {
|
||||
const currentVersion = context.currentVersion!
|
||||
|
||||
// Determine the page type from the pathname
|
||||
const pathParts = pathname.split('/').filter(Boolean)
|
||||
const graphqlIndex = pathParts.indexOf('graphql')
|
||||
|
||||
if (graphqlIndex === -1) {
|
||||
throw new Error(`Invalid GraphQL path: ${pathname}`)
|
||||
}
|
||||
|
||||
const section = pathParts[graphqlIndex + 1] // 'reference' or 'overview'
|
||||
const pageType = pathParts[graphqlIndex + 2] // specific page like 'queries', 'changelog', etc.
|
||||
|
||||
// Handle different GraphQL page types
|
||||
if (section === 'overview' && pageType === 'changelog') {
|
||||
return await this.transformChangelog(page, currentVersion, context)
|
||||
} else if (section === 'overview' && pageType === 'breaking-changes') {
|
||||
return await this.transformBreakingChanges(page, currentVersion, context)
|
||||
} else if (section === 'reference' && pageType) {
|
||||
return await this.transformReference(page, currentVersion, context, pageType)
|
||||
} else if (section === 'reference' && !pageType) {
|
||||
// Index page - just render the intro and manual content
|
||||
return await this.transformIndexPage(page, context)
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported GraphQL page type: ${pathname}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the GraphQL reference index page
|
||||
*/
|
||||
private async transformIndexPage(page: Page, context: Context): Promise<string> {
|
||||
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
|
||||
|
||||
const manualContent = await this.extractManualContent(page, context)
|
||||
|
||||
// Get children links from page metadata
|
||||
const children = page.children || []
|
||||
const childrenLinks = children
|
||||
.map((child) => {
|
||||
const childPath = child.startsWith('/') ? child : `/${child}`
|
||||
const childName = childPath.split('/').pop() || ''
|
||||
const displayName = childName
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
return `- [${displayName}](${childPath})`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const templateData = {
|
||||
pageTitle: page.title,
|
||||
pageIntro: intro,
|
||||
manualContent,
|
||||
childrenLinks,
|
||||
}
|
||||
|
||||
const templatePath = join(__dirname, '../templates/graphql-index.template.md')
|
||||
const templateContent = readFileSync(templatePath, 'utf8')
|
||||
|
||||
return await renderContent(templateContent, {
|
||||
...context,
|
||||
...templateData,
|
||||
markdownRequested: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform GraphQL reference pages (queries, mutations, objects, etc.)
|
||||
*/
|
||||
private async transformReference(
|
||||
page: Page,
|
||||
currentVersion: string,
|
||||
context: Context,
|
||||
pageType: string,
|
||||
): Promise<string> {
|
||||
// Import GraphQL data functions dynamically
|
||||
const { getGraphqlSchema } = await import('@/graphql/lib/index')
|
||||
|
||||
// Map URL-friendly page type to internal schema key
|
||||
const schemaKey = pageType === 'input-objects' ? 'inputObjects' : pageType
|
||||
|
||||
const schema = getGraphqlSchema(currentVersion, schemaKey)
|
||||
|
||||
// Prepare intro and manual content
|
||||
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
|
||||
const manualContent = await this.extractManualContent(page, context)
|
||||
|
||||
// Prepare the schema items based on page type
|
||||
let preparedItems: Array<Record<string, unknown>> = []
|
||||
|
||||
switch (schemaKey) {
|
||||
case 'queries':
|
||||
preparedItems = await Promise.all(
|
||||
(schema as QueryT[]).map((item) => this.prepareQuery(item)),
|
||||
)
|
||||
break
|
||||
case 'mutations':
|
||||
preparedItems = await Promise.all(
|
||||
(schema as MutationT[]).map((item) => this.prepareMutation(item)),
|
||||
)
|
||||
break
|
||||
case 'objects':
|
||||
preparedItems = await Promise.all(
|
||||
(schema as ObjectT[]).map((item) => this.prepareObject(item)),
|
||||
)
|
||||
break
|
||||
case 'interfaces':
|
||||
preparedItems = await Promise.all(
|
||||
(schema as InterfaceT[]).map((item) => this.prepareInterface(item)),
|
||||
)
|
||||
break
|
||||
case 'enums':
|
||||
preparedItems = await Promise.all((schema as EnumT[]).map((item) => this.prepareEnum(item)))
|
||||
break
|
||||
case 'unions':
|
||||
preparedItems = await Promise.all(
|
||||
(schema as UnionT[]).map((item) => this.prepareUnion(item)),
|
||||
)
|
||||
break
|
||||
case 'inputObjects':
|
||||
preparedItems = await Promise.all(
|
||||
(schema as InputObjectT[]).map((item) => this.prepareInputObject(item)),
|
||||
)
|
||||
break
|
||||
case 'scalars':
|
||||
preparedItems = await Promise.all(
|
||||
(schema as ScalarT[]).map((item) => this.prepareScalar(item)),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
const templateData = {
|
||||
pageTitle: page.title,
|
||||
pageIntro: intro,
|
||||
manualContent,
|
||||
items: preparedItems,
|
||||
pageType: schemaKey,
|
||||
}
|
||||
|
||||
const templatePath = join(__dirname, '../templates/graphql-reference.template.md')
|
||||
const templateContent = readFileSync(templatePath, 'utf8')
|
||||
|
||||
return await renderContent(templateContent, {
|
||||
...context,
|
||||
...templateData,
|
||||
markdownRequested: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform changelog page
|
||||
*/
|
||||
private async transformChangelog(
|
||||
page: Page,
|
||||
currentVersion: string,
|
||||
context: Context,
|
||||
): Promise<string> {
|
||||
const { getGraphqlChangelog } = await import('@/graphql/lib/index')
|
||||
|
||||
const schema = getGraphqlChangelog(currentVersion) as ChangelogItemT[]
|
||||
|
||||
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
|
||||
const manualContent = await this.extractManualContent(page, context)
|
||||
|
||||
// Process changelog items
|
||||
const changelogItems = schema.map((item) => {
|
||||
const processChanges = (changes: Array<{ title: string; changes: string[] }>) =>
|
||||
changes.map((change) => ({
|
||||
title: change.title,
|
||||
changes: change.changes.map((html: string) => {
|
||||
// Remove wrapping <p> tags if present
|
||||
if (html.startsWith('<p>') && html.endsWith('</p>')) {
|
||||
return fastTextOnly(html.slice(3, -4))
|
||||
}
|
||||
return fastTextOnly(html)
|
||||
}),
|
||||
}))
|
||||
|
||||
return {
|
||||
date: item.date,
|
||||
schemaChanges: processChanges(item.schemaChanges || []),
|
||||
previewChanges: processChanges(item.previewChanges || []),
|
||||
upcomingChanges: processChanges(item.upcomingChanges || []),
|
||||
}
|
||||
})
|
||||
|
||||
const templateData = {
|
||||
pageTitle: page.title,
|
||||
pageIntro: intro,
|
||||
manualContent,
|
||||
changelogItems,
|
||||
}
|
||||
|
||||
const templatePath = join(__dirname, '../templates/graphql-changelog.template.md')
|
||||
const templateContent = readFileSync(templatePath, 'utf8')
|
||||
|
||||
return await renderContent(templateContent, {
|
||||
...context,
|
||||
...templateData,
|
||||
markdownRequested: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform breaking changes page
|
||||
*/
|
||||
private async transformBreakingChanges(
|
||||
page: Page,
|
||||
currentVersion: string,
|
||||
context: Context,
|
||||
): Promise<string> {
|
||||
const { getGraphqlBreakingChanges } = await import('@/graphql/lib/index')
|
||||
|
||||
const schema = getGraphqlBreakingChanges(currentVersion) as BreakingChangesT
|
||||
|
||||
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
|
||||
const manualContent = await this.extractManualContent(page, context)
|
||||
|
||||
const slugger = new GithubSlugger()
|
||||
|
||||
// Process breaking changes by date
|
||||
const breakingChangesByDate = Object.keys(schema).map((date) => {
|
||||
const items = schema[date]
|
||||
const heading = `Changes scheduled for ${date}`
|
||||
const slug = slugger.slug(heading)
|
||||
|
||||
return {
|
||||
date,
|
||||
heading,
|
||||
slug,
|
||||
items: items.map((item) => ({
|
||||
location: item.location,
|
||||
description: fastTextOnly(item.description),
|
||||
reason: fastTextOnly(item.reason),
|
||||
criticality: item.criticality,
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
const templateData = {
|
||||
pageTitle: page.title,
|
||||
pageIntro: intro,
|
||||
manualContent,
|
||||
breakingChangesByDate,
|
||||
}
|
||||
|
||||
const templatePath = join(__dirname, '../templates/graphql-breaking-changes.template.md')
|
||||
const templateContent = readFileSync(templatePath, 'utf8')
|
||||
|
||||
return await renderContent(templateContent, {
|
||||
...context,
|
||||
...templateData,
|
||||
markdownRequested: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract manual content from page markdown
|
||||
*/
|
||||
private async extractManualContent(page: Page, context: Context): Promise<string> {
|
||||
if (!page.markdown) return ''
|
||||
|
||||
const markerIndex = page.markdown.indexOf(
|
||||
'<!-- Content after this section is automatically generated -->',
|
||||
)
|
||||
|
||||
if (markerIndex <= 0) return ''
|
||||
|
||||
const { content } = matter(page.markdown)
|
||||
const manualContentMarkerIndex = content.indexOf(
|
||||
'<!-- Content after this section is automatically generated -->',
|
||||
)
|
||||
|
||||
if (manualContentMarkerIndex <= 0) return ''
|
||||
|
||||
const rawManualContent = content.substring(0, manualContentMarkerIndex).trim()
|
||||
if (!rawManualContent) return ''
|
||||
|
||||
return await renderContent(rawManualContent, {
|
||||
...context,
|
||||
markdownRequested: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a query item for rendering
|
||||
*/
|
||||
private async prepareQuery(query: QueryT): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
name: query.name,
|
||||
slug: query.name.toLowerCase(),
|
||||
description: query.description ? fastTextOnly(query.description) : '',
|
||||
type: query.type,
|
||||
href: query.href,
|
||||
isDeprecated: query.isDeprecated || false,
|
||||
deprecationReason: query.deprecationReason
|
||||
? fastTextOnly(query.deprecationReason)
|
||||
: undefined,
|
||||
args: query.args.map((arg) => ({
|
||||
name: arg.name,
|
||||
type: arg.type,
|
||||
href: arg.href,
|
||||
description: arg.description ? fastTextOnly(arg.description) : '',
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a mutation item for rendering
|
||||
*/
|
||||
private async prepareMutation(mutation: MutationT): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
name: mutation.name,
|
||||
slug: mutation.name.toLowerCase(),
|
||||
description: mutation.description ? fastTextOnly(mutation.description) : '',
|
||||
isDeprecated: mutation.isDeprecated || false,
|
||||
deprecationReason: mutation.deprecationReason
|
||||
? fastTextOnly(mutation.deprecationReason)
|
||||
: undefined,
|
||||
inputFields: await this.prepareFields(mutation.inputFields),
|
||||
returnFields: await this.prepareFields(mutation.returnFields),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an object item for rendering
|
||||
*/
|
||||
private async prepareObject(object: ObjectT): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
name: object.name,
|
||||
slug: object.name.toLowerCase(),
|
||||
description: object.description ? fastTextOnly(object.description) : '',
|
||||
isDeprecated: object.isDeprecated || false,
|
||||
deprecationReason: object.deprecationReason
|
||||
? fastTextOnly(object.deprecationReason)
|
||||
: undefined,
|
||||
implements: object.implements || [],
|
||||
fields: await this.prepareFields(object.fields),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an interface item for rendering
|
||||
*/
|
||||
private async prepareInterface(item: InterfaceT): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
name: item.name,
|
||||
slug: item.name.toLowerCase(),
|
||||
description: item.description ? fastTextOnly(item.description) : '',
|
||||
isDeprecated: item.isDeprecated || false,
|
||||
deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
|
||||
fields: await this.prepareFields(item.fields),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an enum item for rendering
|
||||
*/
|
||||
private async prepareEnum(item: EnumT): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
name: item.name,
|
||||
slug: item.name.toLowerCase(),
|
||||
description: item.description ? fastTextOnly(item.description) : '',
|
||||
isDeprecated: item.isDeprecated || false,
|
||||
deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
|
||||
values: item.values.map((value) => ({
|
||||
name: value.name,
|
||||
description: value.description ? fastTextOnly(value.description) : '',
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a union item for rendering
|
||||
*/
|
||||
private async prepareUnion(item: UnionT): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
name: item.name,
|
||||
slug: item.name.toLowerCase(),
|
||||
description: item.description ? fastTextOnly(item.description) : '',
|
||||
isDeprecated: item.isDeprecated || false,
|
||||
deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
|
||||
possibleTypes: item.possibleTypes || [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an input object item for rendering
|
||||
*/
|
||||
private async prepareInputObject(item: InputObjectT): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
name: item.name,
|
||||
slug: item.name.toLowerCase(),
|
||||
description: item.description ? fastTextOnly(item.description) : '',
|
||||
isDeprecated: item.isDeprecated || false,
|
||||
deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
|
||||
inputFields: await this.prepareFields(item.inputFields),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a scalar item for rendering
|
||||
*/
|
||||
private async prepareScalar(item: ScalarT): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
name: item.name,
|
||||
slug: item.name.toLowerCase(),
|
||||
description: item.description ? fastTextOnly(item.description) : '',
|
||||
isDeprecated: item.isDeprecated || false,
|
||||
deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare fields for rendering
|
||||
*/
|
||||
private async prepareFields(fields: FieldT[]): Promise<Array<Record<string, unknown>>> {
|
||||
return fields.map((field) => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
href: field.href,
|
||||
description: field.description ? fastTextOnly(field.description) : '',
|
||||
defaultValue: field.defaultValue,
|
||||
isDeprecated: field.isDeprecated || false,
|
||||
deprecationReason: field.deprecationReason
|
||||
? fastTextOnly(field.deprecationReason)
|
||||
: undefined,
|
||||
arguments: field.arguments
|
||||
? field.arguments.map((arg) => ({
|
||||
name: arg.name,
|
||||
description: arg.description ? fastTextOnly(arg.description) : '',
|
||||
defaultValue: arg.defaultValue,
|
||||
type: {
|
||||
name: arg.type.name,
|
||||
href: arg.type.href,
|
||||
},
|
||||
}))
|
||||
: undefined,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TransformerRegistry } from './types'
|
||||
import { RestTransformer } from './rest-transformer'
|
||||
import { GraphQLTransformer } from './graphql-transformer'
|
||||
|
||||
/**
|
||||
* Global transformer registry
|
||||
@@ -10,6 +11,9 @@ export const transformerRegistry = new TransformerRegistry()
|
||||
// Register REST transformer
|
||||
transformerRegistry.register(new RestTransformer())
|
||||
|
||||
// Register GraphQL transformer
|
||||
transformerRegistry.register(new GraphQLTransformer())
|
||||
|
||||
// Future transformers can be registered here:
|
||||
// transformerRegistry.register(new WebhooksTransformer())
|
||||
// transformerRegistry.register(new GitHubAppsTransformer())
|
||||
|
||||
12
src/fixtures/fixtures/content/graphql/index.md
Normal file
12
src/fixtures/fixtures/content/graphql/index.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: GitHub GraphQL API documentation
|
||||
intro: 'To create integrations, retrieve data, and automate your workflows, use the {% data variables.product.prodname_dotcom %} GraphQL API.'
|
||||
shortTitle: GraphQL API
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
children:
|
||||
- /overview
|
||||
- /reference
|
||||
---
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Breaking changes
|
||||
intro: Learn about recent and upcoming breaking changes to the {% data variables.product.prodname_dotcom %} GraphQL API.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
## About breaking changes
|
||||
|
||||
Breaking changes are any changes that might require action from our integrators. We divide these changes into two categories:
|
||||
|
||||
* **Breaking:** Changes that will break existing queries to the GraphQL API. For example, removing a field would be a breaking change.
|
||||
* **Dangerous:** Changes that won't break existing queries but could affect the runtime behavior of clients. Adding an enum value is an example of a dangerous change.
|
||||
|
||||
We'll announce upcoming breaking changes at least three months before making changes to the GraphQL schema, to give integrators time to make the necessary adjustments.
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
13
src/fixtures/fixtures/content/graphql/overview/changelog.md
Normal file
13
src/fixtures/fixtures/content/graphql/overview/changelog.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Changelog
|
||||
intro: The GraphQL schema changelog is a list of recent and upcoming changes to our GraphQL API schema.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
Breaking changes include changes that will break existing queries or could affect the runtime behavior of clients. For a list of breaking changes and when they will occur, see our breaking changes log.
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
15
src/fixtures/fixtures/content/graphql/overview/index.md
Normal file
15
src/fixtures/fixtures/content/graphql/overview/index.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Overview
|
||||
intro: Learn about the GraphQL API overview topics.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
children:
|
||||
- /breaking-changes
|
||||
- /changelog
|
||||
---
|
||||
|
||||
## GraphQL API overview
|
||||
|
||||
The GraphQL API provides a powerful way to query GitHub data.
|
||||
17
src/fixtures/fixtures/content/graphql/reference/enums.md
Normal file
17
src/fixtures/fixtures/content/graphql/reference/enums.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Enums
|
||||
intro: Enums represent possible sets of values for a field.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
## About enums
|
||||
|
||||
Enums are special types that define a set of possible values. When a field has an enum type, it can only be set to one of the predefined values.
|
||||
|
||||
For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
20
src/fixtures/fixtures/content/graphql/reference/index.md
Normal file
20
src/fixtures/fixtures/content/graphql/reference/index.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Reference
|
||||
intro: View reference documentation to learn about the data types available in the {% data variables.product.prodname_dotcom %} GraphQL API schema.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
children:
|
||||
- /queries
|
||||
- /mutations
|
||||
- /objects
|
||||
- /interfaces
|
||||
- /enums
|
||||
- /unions
|
||||
- /input-objects
|
||||
- /scalars
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Input objects
|
||||
intro: Input objects are special types that allow you to pass complex objects as arguments to queries and mutations.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
## About input objects
|
||||
|
||||
Input objects are used when you need to pass a structured set of values as an argument to a field. They are similar to regular objects, but they are specifically designed to be used as input arguments.
|
||||
|
||||
For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Interfaces
|
||||
intro: Interfaces are abstract types that include a certain set of fields that other types must include if they implement the interface.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
## About interfaces
|
||||
|
||||
Interfaces allow you to define a set of fields that multiple object types can implement. They help ensure consistency across your schema.
|
||||
|
||||
For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
17
src/fixtures/fixtures/content/graphql/reference/mutations.md
Normal file
17
src/fixtures/fixtures/content/graphql/reference/mutations.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Mutations
|
||||
intro: The mutation type defines GraphQL operations that create, update, or delete data on the server.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
## About mutations
|
||||
|
||||
Every GraphQL schema has a root type for both queries and mutations. The mutation type defines GraphQL operations that modify data on the server.
|
||||
|
||||
For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
17
src/fixtures/fixtures/content/graphql/reference/objects.md
Normal file
17
src/fixtures/fixtures/content/graphql/reference/objects.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Objects
|
||||
intro: Objects in GraphQL represent the resources you can access. An object can contain a list of fields, which are specifically typed.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
## About objects
|
||||
|
||||
Objects are the most common type in a GraphQL schema. They represent the resources you can access and contain fields that define the data you can query.
|
||||
|
||||
For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
17
src/fixtures/fixtures/content/graphql/reference/queries.md
Normal file
17
src/fixtures/fixtures/content/graphql/reference/queries.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Queries
|
||||
intro: The query type defines GraphQL operations that retrieve data from the server.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
## About queries
|
||||
|
||||
Every GraphQL schema has a root type for both queries and mutations. The query type defines GraphQL operations that retrieve data from the server.
|
||||
|
||||
For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
17
src/fixtures/fixtures/content/graphql/reference/scalars.md
Normal file
17
src/fixtures/fixtures/content/graphql/reference/scalars.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Scalars
|
||||
intro: Scalars are primitive values in GraphQL. They represent the leaves of a query.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
## About scalars
|
||||
|
||||
Scalars are primitive types that represent concrete values. GraphQL comes with a set of default scalar types, including String, Int, Float, Boolean, and ID.
|
||||
|
||||
For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
17
src/fixtures/fixtures/content/graphql/reference/unions.md
Normal file
17
src/fixtures/fixtures/content/graphql/reference/unions.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Unions
|
||||
intro: Unions represent an object that could be one of multiple types.
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghec: '*'
|
||||
ghes: '*'
|
||||
autogenerated: graphql
|
||||
---
|
||||
|
||||
## About unions
|
||||
|
||||
A union type is a special type that represents an object that could be one of a listed set of types. When you query a field that returns a union type, you need to use conditional fragments to query any fields.
|
||||
|
||||
For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world).
|
||||
|
||||
<!-- Content after this section is automatically generated -->
|
||||
@@ -32,6 +32,7 @@ children:
|
||||
- actions
|
||||
- rest
|
||||
- webhooks
|
||||
- graphql
|
||||
- video-transcripts
|
||||
# - account-and-profile
|
||||
# - authentication
|
||||
|
||||
@@ -388,6 +388,7 @@ export type Page = {
|
||||
sidebarLink?: SidebarLink
|
||||
type?: string
|
||||
contentType?: string
|
||||
children?: string[]
|
||||
}
|
||||
|
||||
export type SidebarLink = {
|
||||
|
||||
Reference in New Issue
Block a user