1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Convert 6 files to TypeScript (#58003)

This commit is contained in:
Kevin Heis
2025-10-22 08:32:11 -07:00
committed by GitHub
parent 0087c57e7b
commit d258ab56d1
8 changed files with 302 additions and 149 deletions

View File

@@ -1,9 +1,38 @@
import { describe, expect, test } from 'vitest' import { describe, expect, test } from 'vitest'
import { shouldFilterMetadataPermission, calculateAdditionalPermissions } from '../scripts/sync' import { shouldFilterMetadataPermission, calculateAdditionalPermissions } from '../scripts/sync'
type PermissionSet = Record<string, string>
interface Operation {
operationId: string
permissionSets: PermissionSet[]
}
interface ProgAccessData {
userToServerRest: boolean
serverToServer: boolean
permissions: PermissionSet[]
}
interface ActorResource {
title: string
visibility: string
}
interface FilteredOperation {
operationId: string
permission: string
additionalPermissions: boolean
}
interface MetadataPermission {
operationId: string
additionalPermissions: boolean
}
describe('metadata permissions filtering', () => { describe('metadata permissions filtering', () => {
// Mock data structure representing operations with metadata permissions // Mock data structure representing operations with metadata permissions
const mockOperationsWithMetadata = [ const mockOperationsWithMetadata: Operation[] = [
{ {
operationId: 'repos/enable-automated-security-fixes', operationId: 'repos/enable-automated-security-fixes',
permissionSets: [{ metadata: 'read', administration: 'write' }], permissionSets: [{ metadata: 'read', administration: 'write' }],
@@ -23,7 +52,7 @@ describe('metadata permissions filtering', () => {
] ]
// Mock programmatic access data // Mock programmatic access data
const mockProgAccessData = { const mockProgAccessData: Record<string, ProgAccessData> = {
'repos/enable-automated-security-fixes': { 'repos/enable-automated-security-fixes': {
userToServerRest: true, userToServerRest: true,
serverToServer: true, serverToServer: true,
@@ -47,7 +76,7 @@ describe('metadata permissions filtering', () => {
} }
// Mock actor resources // Mock actor resources
const mockProgActorResources = { const mockProgActorResources: Record<string, ActorResource> = {
metadata: { metadata: {
title: 'Metadata', title: 'Metadata',
visibility: 'public', visibility: 'public',
@@ -95,8 +124,8 @@ describe('metadata permissions filtering', () => {
}) })
test('filters metadata operations with additional permissions', () => { test('filters metadata operations with additional permissions', () => {
const filteredOperations = [] const filteredOperations: FilteredOperation[] = []
const metadataPermissions = [] const metadataPermissions: MetadataPermission[] = []
for (const operation of mockOperationsWithMetadata) { for (const operation of mockOperationsWithMetadata) {
const progData = mockProgAccessData[operation.operationId] const progData = mockProgAccessData[operation.operationId]
@@ -137,15 +166,15 @@ describe('metadata permissions filtering', () => {
// Should have other permissions from operations with additional permissions // Should have other permissions from operations with additional permissions
const adminPermission = filteredOperations.find((op) => op.permission === 'administration') const adminPermission = filteredOperations.find((op) => op.permission === 'administration')
expect(adminPermission).toBeDefined() expect(adminPermission).toBeDefined()
expect(adminPermission.operationId).toBe('repos/enable-automated-security-fixes') expect(adminPermission!.operationId).toBe('repos/enable-automated-security-fixes')
expect(adminPermission.additionalPermissions).toBe(true) expect(adminPermission!.additionalPermissions).toBe(true)
const orgAdminPermission = filteredOperations.find( const orgAdminPermission = filteredOperations.find(
(op) => op.permission === 'organization_administration', (op) => op.permission === 'organization_administration',
) )
expect(orgAdminPermission).toBeDefined() expect(orgAdminPermission).toBeDefined()
expect(orgAdminPermission.operationId).toBe('orgs/update-webhook') expect(orgAdminPermission!.operationId).toBe('orgs/update-webhook')
expect(orgAdminPermission.additionalPermissions).toBe(true) expect(orgAdminPermission!.additionalPermissions).toBe(true)
}) })
test('preserves non-metadata permissions regardless of additional permissions', () => { test('preserves non-metadata permissions regardless of additional permissions', () => {
@@ -168,11 +197,11 @@ describe('metadata permissions filtering', () => {
expect(shouldFilterMetadataPermission('metadata', [])).toBe(false) expect(shouldFilterMetadataPermission('metadata', [])).toBe(false)
// Permission set with empty object (edge case) // Permission set with empty object (edge case)
const edgeCase1 = [{ metadata: 'read' }, {}] const edgeCase1: Record<string, string>[] = [{ metadata: 'read' }, {}]
expect(shouldFilterMetadataPermission('metadata', edgeCase1)).toBe(true) expect(shouldFilterMetadataPermission('metadata', edgeCase1)).toBe(true)
// Multiple permission sets with metadata in different sets // Multiple permission sets with metadata in different sets
const edgeCase2 = [{ metadata: 'read' }, { admin: 'write' }] const edgeCase2: Record<string, string>[] = [{ metadata: 'read' }, { admin: 'write' }]
expect(shouldFilterMetadataPermission('metadata', edgeCase2)).toBe(true) expect(shouldFilterMetadataPermission('metadata', edgeCase2)).toBe(true)
}) })
@@ -207,17 +236,23 @@ describe('metadata permissions filtering', () => {
test('handles complex permission structures from real data', () => { test('handles complex permission structures from real data', () => {
// Multiple permission sets (should filter metadata) // Multiple permission sets (should filter metadata)
const multiplePermissionSets = [{ metadata: 'read' }, { administration: 'write' }] const multiplePermissionSets: Record<string, string>[] = [
{ metadata: 'read' },
{ administration: 'write' },
]
expect(shouldFilterMetadataPermission('metadata', multiplePermissionSets)).toBe(true) expect(shouldFilterMetadataPermission('metadata', multiplePermissionSets)).toBe(true)
// Single permission set with multiple permissions (should filter metadata) // Single permission set with multiple permissions (should filter metadata)
const multiplePermissionsInSet = [ const multiplePermissionsInSet: Record<string, string>[] = [
{ metadata: 'read', contents: 'write', pull_requests: 'write' }, { metadata: 'read', contents: 'write', pull_requests: 'write' },
] ]
expect(shouldFilterMetadataPermission('metadata', multiplePermissionsInSet)).toBe(true) expect(shouldFilterMetadataPermission('metadata', multiplePermissionsInSet)).toBe(true)
// Multiple permission sets where metadata is not in the first set // Multiple permission sets where metadata is not in the first set
const metadataInSecondSet = [{ administration: 'write' }, { metadata: 'read' }] const metadataInSecondSet: Record<string, string>[] = [
{ administration: 'write' },
{ metadata: 'read' },
]
expect(shouldFilterMetadataPermission('metadata', metadataInSecondSet)).toBe(true) expect(shouldFilterMetadataPermission('metadata', metadataInSecondSet)).toBe(true)
}) })
@@ -250,7 +285,7 @@ describe('metadata permissions filtering', () => {
}) })
test('verifies consistency with additional-permissions flag calculation', () => { test('verifies consistency with additional-permissions flag calculation', () => {
const testCases = [ const testCases: Array<{ permissionSets: Record<string, string>[]; expected: boolean }> = [
// Single permission, single set - no additional permissions // Single permission, single set - no additional permissions
{ permissionSets: [{ metadata: 'read' }], expected: false }, { permissionSets: [{ metadata: 'read' }], expected: false },
@@ -283,12 +318,7 @@ describe('metadata permissions filtering', () => {
// - DELETE /orgs/{org}/actions/permissions/repositories/{repository_id} // - DELETE /orgs/{org}/actions/permissions/repositories/{repository_id}
// Because they have metadata + organization_administration permissions // Because they have metadata + organization_administration permissions
const mockMutatingOperation = { const progData: ProgAccessData = {
operationId: 'actions/set-selected-repositories-enabled-github-actions-organization',
permissionSets: [{ metadata: 'read', organization_administration: 'write' }],
}
const progData = {
userToServerRest: true, userToServerRest: true,
serverToServer: true, serverToServer: true,
permissions: [{ metadata: 'read', organization_administration: 'write' }], permissions: [{ metadata: 'read', organization_administration: 'write' }],

View File

@@ -1,23 +1,74 @@
import { diff, ChangeType } from '@graphql-inspector/core' import { diff, ChangeType, Change } from '@graphql-inspector/core'
import { loadSchema } from '@graphql-tools/load' import { loadSchema } from '@graphql-tools/load'
import fs from 'fs' import fs from 'fs'
import { renderContent } from '@/content-render/index' import { renderContent } from '@/content-render/index'
interface UpcomingChange {
location: string
date: string
description: string
}
interface Preview {
title: string
toggled_on: string[]
}
interface ChangelogSchemaChange {
title: string
changes: string[]
}
interface ChangelogPreviewChange {
title: string
changes: string[]
}
interface ChangelogUpcomingChange {
title: string
changes: string[]
}
export interface ChangelogEntry {
date?: string
schemaChanges: ChangelogSchemaChange[]
previewChanges: ChangelogPreviewChange[]
upcomingChanges: ChangelogUpcomingChange[]
}
interface PreviewChanges {
title: string
changes: Change[]
}
interface SegmentedChanges {
schemaChangesToReport: Change[]
previewChangesToReport: Record<string, PreviewChanges>
}
interface IgnoredChangeType {
type: string
count: number
}
interface IgnoredChangesSummary {
totalCount: number
typeCount: number
types: IgnoredChangeType[]
}
/** /**
* Tag `changelogEntry` with `date: YYYY-mm-dd`, then prepend it to the JSON * Tag `changelogEntry` with `date: YYYY-mm-dd`, then prepend it to the JSON
* structure written to `targetPath`. (`changelogEntry` and that file are modified in place.) * structure written to `targetPath`. (`changelogEntry` and that file are modified in place.)
* @param {object} changelogEntry
* @param {string} targetPath
* @return {void}
*/ */
export function prependDatedEntry(changelogEntry, targetPath) { export function prependDatedEntry(changelogEntry: ChangelogEntry, targetPath: string): void {
// Build a `yyyy-mm-dd`-formatted date string // Build a `yyyy-mm-dd`-formatted date string
// and tag the changelog entry with it // and tag the changelog entry with it
const todayString = new Date().toISOString().slice(0, 10) const todayString = new Date().toISOString().slice(0, 10)
changelogEntry.date = todayString changelogEntry.date = todayString
const previousChangelogString = fs.readFileSync(targetPath) const previousChangelogString = fs.readFileSync(targetPath, 'utf8')
const previousChangelog = JSON.parse(previousChangelogString) const previousChangelog = JSON.parse(previousChangelogString) as ChangelogEntry[]
// add a new entry to the changelog data // add a new entry to the changelog data
previousChangelog.unshift(changelogEntry) previousChangelog.unshift(changelogEntry)
// rewrite the updated changelog // rewrite the updated changelog
@@ -29,28 +80,23 @@ export function prependDatedEntry(changelogEntry, targetPath) {
* changes that warrant a changelog entry, return a changelog entry. * changes that warrant a changelog entry, return a changelog entry.
* Based on the parsed `previews`, identify changes that are under a preview. * Based on the parsed `previews`, identify changes that are under a preview.
* Otherwise, return null. * Otherwise, return null.
* @param {string} [oldSchemaString]
* @param {string} [newSchemaString]
* @param {Array<object>} [previews]
* @param {Array<object>} [oldUpcomingChanges]
* @param {Array<object>} [newUpcomingChanges]
* @return {object?}
*/ */
export async function createChangelogEntry( export async function createChangelogEntry(
oldSchemaString, oldSchemaString: string,
newSchemaString, newSchemaString: string,
previews, previews: Preview[],
oldUpcomingChanges, oldUpcomingChanges: UpcomingChange[],
newUpcomingChanges, newUpcomingChanges: UpcomingChange[],
) { ): Promise<ChangelogEntry | null> {
// Create schema objects out of the strings // Create schema objects out of the strings
const oldSchema = await loadSchema(oldSchemaString, {}) // Using 'as any' because loadSchema accepts string schema directly without requiring loaders
const newSchema = await loadSchema(newSchemaString, {}) const oldSchema = await loadSchema(oldSchemaString, {} as any)
const newSchema = await loadSchema(newSchemaString, {} as any)
// Generate changes between the two schemas // Generate changes between the two schemas
const changes = await diff(oldSchema, newSchema) const changes = await diff(oldSchema, newSchema)
const changesToReport = [] const changesToReport: Change[] = []
const ignoredChanges = [] const ignoredChanges: Change[] = []
changes.forEach((change) => { changes.forEach((change) => {
if (CHANGES_TO_REPORT.includes(change.type)) { if (CHANGES_TO_REPORT.includes(change.type)) {
changesToReport.push(change) changesToReport.push(change)
@@ -76,14 +122,14 @@ export async function createChangelogEntry(
} }
// Store ignored changes for potential workflow outputs // Store ignored changes for potential workflow outputs
createChangelogEntry.lastIgnoredChanges = ignoredChanges ;(createChangelogEntry as any).lastIgnoredChanges = ignoredChanges
const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges( const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges(
changesToReport, changesToReport,
previews, previews,
) )
const addedUpcomingChanges = newUpcomingChanges.filter(function (change) { const addedUpcomingChanges = newUpcomingChanges.filter(function (change): boolean {
// Manually check each of `newUpcomingChanges` for an equivalent entry // Manually check each of `newUpcomingChanges` for an equivalent entry
// in `oldUpcomingChanges`. // in `oldUpcomingChanges`.
return !oldUpcomingChanges.find(function (oldChange) { return !oldUpcomingChanges.find(function (oldChange) {
@@ -98,10 +144,10 @@ export async function createChangelogEntry(
// If there were any changes, create a changelog entry // If there were any changes, create a changelog entry
if ( if (
schemaChangesToReport.length > 0 || schemaChangesToReport.length > 0 ||
previewChangesToReport.length > 0 || Object.keys(previewChangesToReport).length > 0 ||
addedUpcomingChanges.length > 0 addedUpcomingChanges.length > 0
) { ) {
const changelogEntry = { const changelogEntry: ChangelogEntry = {
schemaChanges: [], schemaChanges: [],
previewChanges: [], previewChanges: [],
upcomingChanges: [], upcomingChanges: [],
@@ -109,11 +155,11 @@ export async function createChangelogEntry(
const cleanedSchemaChanges = cleanMessagesFromChanges(schemaChangesToReport) const cleanedSchemaChanges = cleanMessagesFromChanges(schemaChangesToReport)
const renderedScheamChanges = await Promise.all( const renderedScheamChanges = await Promise.all(
cleanedSchemaChanges.map(async (change) => { cleanedSchemaChanges.map(async (change): Promise<string> => {
return await renderContent(change) return await renderContent(change)
}), }),
) )
const schemaChange = { const schemaChange: ChangelogSchemaChange = {
title: 'The GraphQL schema includes these changes:', title: 'The GraphQL schema includes these changes:',
// Replace single quotes which wrap field/argument/type names with backticks // Replace single quotes which wrap field/argument/type names with backticks
changes: renderedScheamChanges, changes: renderedScheamChanges,
@@ -124,7 +170,7 @@ export async function createChangelogEntry(
const previewChanges = previewChangesToReport[previewTitle] const previewChanges = previewChangesToReport[previewTitle]
const cleanedPreviewChanges = cleanMessagesFromChanges(previewChanges.changes) const cleanedPreviewChanges = cleanMessagesFromChanges(previewChanges.changes)
const renderedPreviewChanges = await Promise.all( const renderedPreviewChanges = await Promise.all(
cleanedPreviewChanges.map(async (change) => { cleanedPreviewChanges.map(async (change): Promise<string> => {
return renderContent(change) return renderContent(change)
}), }),
) )
@@ -146,10 +192,10 @@ export async function createChangelogEntry(
const location = change.location const location = change.location
const description = change.description const description = change.description
const date = change.date.split('T')[0] const date = change.date.split('T')[0]
return 'On member `' + location + '`:' + description + ' **Effective ' + date + '**.' return `On member \`${location}\`:${description} **Effective ${date}**.`
}) })
const renderedUpcomingChanges = await Promise.all( const renderedUpcomingChanges = await Promise.all(
cleanedUpcomingChanges.map(async (change) => { cleanedUpcomingChanges.map(async (change): Promise<string> => {
return await renderContent(change) return await renderContent(change)
}), }),
) )
@@ -167,10 +213,8 @@ export async function createChangelogEntry(
/** /**
* Prepare the preview title from github/github source for the docs. * Prepare the preview title from github/github source for the docs.
* @param {string} title
* @return {string}
*/ */
export function cleanPreviewTitle(title) { export function cleanPreviewTitle(title: string): string {
if (title === 'UpdateRefsPreview') { if (title === 'UpdateRefsPreview') {
title = 'Update refs preview' title = 'Update refs preview'
} else if (title === 'MergeInfoPreview') { } else if (title === 'MergeInfoPreview') {
@@ -184,10 +228,8 @@ export function cleanPreviewTitle(title) {
/** /**
* Turn the given title into an HTML-ready anchor. * Turn the given title into an HTML-ready anchor.
* (ported from graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L281) * (ported from graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L281)
* @param {string} [previewTitle]
* @return {string}
*/ */
export function previewAnchor(previewTitle) { export function previewAnchor(previewTitle: string): string {
return previewTitle return previewTitle
.toLowerCase() .toLowerCase()
.replace(/ /g, '-') .replace(/ /g, '-')
@@ -196,11 +238,9 @@ export function previewAnchor(previewTitle) {
/** /**
* Turn changes from graphql-inspector into messages for the HTML changelog. * Turn changes from graphql-inspector into messages for the HTML changelog.
* @param {Array<object>} changes
* @return {Array<string>}
*/ */
export function cleanMessagesFromChanges(changes) { export function cleanMessagesFromChanges(changes: Change[]): string[] {
return changes.map(function (change) { return changes.map(function (change): string {
// replace single quotes around graphql names with backticks, // replace single quotes around graphql names with backticks,
// to match previous behavior from graphql-schema-comparator // to match previous behavior from graphql-schema-comparator
return change.message.replace(/'([a-zA-Z. :!]+)'/g, '`$1`') return change.message.replace(/'([a-zA-Z. :!]+)'/g, '`$1`')
@@ -212,29 +252,29 @@ export function cleanMessagesFromChanges(changes) {
* one for changes in the main schema, * one for changes in the main schema,
* and another for changes that are under preview. * and another for changes that are under preview.
* (Ported from /graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L230) * (Ported from /graphql-docs/lib/graphql_docs/update_internal_developer/change_log.rb#L230)
* @param {Array<object>} changesToReport
* @param {object} previews
* @return {object}
*/ */
export function segmentPreviewChanges(changesToReport, previews) { export function segmentPreviewChanges(
changesToReport: Change[],
previews: Preview[],
): SegmentedChanges {
// Build a map of `{ path => previewTitle` } // Build a map of `{ path => previewTitle` }
// for easier lookup of change to preview // for easier lookup of change to preview
const pathToPreview = {} const pathToPreview: Record<string, string> = {}
previews.forEach(function (preview) { previews.forEach(function (preview): void {
preview.toggled_on.forEach(function (path) { preview.toggled_on.forEach(function (path) {
pathToPreview[path] = preview.title pathToPreview[path] = preview.title
}) })
}) })
const schemaChanges = [] const schemaChanges: Change[] = []
const changesByPreview = {} const changesByPreview: Record<string, PreviewChanges> = {}
changesToReport.forEach(function (change) { changesToReport.forEach(function (change): void {
// For each change, see if its path _or_ one of its ancestors // For each change, see if its path _or_ one of its ancestors
// is covered by a preview. If it is, mark this change as belonging to a preview // is covered by a preview. If it is, mark this change as belonging to a preview
const pathParts = change.path.split('.') const pathParts = change.path?.split('.') || []
let testPath = null let testPath: string | null = null
let previewTitle = null let previewTitle: string | null = null
let previewChanges = null let previewChanges: PreviewChanges | null = null
while (pathParts.length > 0 && !previewTitle) { while (pathParts.length > 0 && !previewTitle) {
testPath = pathParts.join('.') testPath = pathParts.join('.')
previewTitle = pathToPreview[testPath] previewTitle = pathToPreview[testPath]
@@ -294,28 +334,28 @@ const CHANGES_TO_REPORT = [
/** /**
* Get the ignored change types from the last changelog entry creation * Get the ignored change types from the last changelog entry creation
* @returns {Array} Array of ignored change objects
*/ */
export function getLastIgnoredChanges() { export function getLastIgnoredChanges(): Change[] {
return createChangelogEntry.lastIgnoredChanges || [] return (createChangelogEntry as any).lastIgnoredChanges || []
} }
/** /**
* Get summary of ignored change types for workflow outputs * Get summary of ignored change types for workflow outputs
* @returns {Object} Summary with counts and types
*/ */
export function getIgnoredChangesSummary() { export function getIgnoredChangesSummary(): IgnoredChangesSummary | null {
const ignored = getLastIgnoredChanges() const ignored = getLastIgnoredChanges()
if (ignored.length === 0) return null if (ignored.length === 0) return null
const types = [...new Set(ignored.map((change) => change.type))] const types = [...new Set(ignored.map((change) => change.type))]
const summary = { const summary: IgnoredChangesSummary = {
totalCount: ignored.length, totalCount: ignored.length,
typeCount: types.length, typeCount: types.length,
types: types.map((type) => ({ types: types.map(
(type): IgnoredChangeType => ({
type, type,
count: ignored.filter((change) => change.type === type).length, count: ignored.filter((change) => change.type === type).length,
})), }),
),
} }
return summary return summary

View File

@@ -11,6 +11,7 @@ import {
prependDatedEntry, prependDatedEntry,
getLastIgnoredChanges, getLastIgnoredChanges,
getIgnoredChangesSummary, getIgnoredChangesSummary,
type ChangelogEntry,
} from '../scripts/build-changelog' } from '../scripts/build-changelog'
import readJsonFile from '@/frame/lib/read-json-file' import readJsonFile from '@/frame/lib/read-json-file'
@@ -30,10 +31,6 @@ interface UpcomingChange {
date: string date: string
} }
interface ChangelogEntry {
[key: string]: any
}
interface IgnoredChange { interface IgnoredChange {
type: string type: string
[key: string]: any [key: string]: any
@@ -245,7 +242,11 @@ describe('updating the changelog file', () => {
const testTargetPath = 'src/graphql/tests/fixtures/example-changelog.json' const testTargetPath = 'src/graphql/tests/fixtures/example-changelog.json'
const previousContents = await fs.readFile(testTargetPath) const previousContents = await fs.readFile(testTargetPath)
const exampleEntry: ChangelogEntry = { someStuff: true } const exampleEntry: ChangelogEntry = {
schemaChanges: [],
previewChanges: [],
upcomingChanges: [],
}
const expectedDate = '2020-11-20' const expectedDate = '2020-11-20'
MockDate.set(expectedDate) MockDate.set(expectedDate)
@@ -254,7 +255,12 @@ describe('updating the changelog file', () => {
// reset the file: // reset the file:
await fs.writeFile(testTargetPath, previousContents.toString()) await fs.writeFile(testTargetPath, previousContents.toString())
expect(exampleEntry).toEqual({ someStuff: true, date: expectedDate }) expect(exampleEntry).toEqual({
schemaChanges: [],
previewChanges: [],
upcomingChanges: [],
date: expectedDate,
})
expect(JSON.parse(newContents)).toEqual(expectedUpdatedChangelogFile) expect(JSON.parse(newContents)).toEqual(expectedUpdatedChangelogFile)
}) })
}) })

View File

@@ -1,7 +1,9 @@
[ [
{ {
"someStuff": true, "date": "2020-11-20",
"date": "2020-11-20" "schemaChanges": [],
"previewChanges": [],
"upcomingChanges": []
}, },
{ {
"previous_entry": "..." "previous_entry": "..."

View File

@@ -5,11 +5,54 @@ const DEFAULT_EXAMPLE_DESCRIPTION = 'Example'
const DEFAULT_EXAMPLE_KEY = 'default' const DEFAULT_EXAMPLE_KEY = 'default'
const DEFAULT_ACCEPT_HEADER = 'application/vnd.github.v3+json' const DEFAULT_ACCEPT_HEADER = 'application/vnd.github.v3+json'
// OpenAPI operation structure is dynamic and complex
type Operation = any
interface RequestExample {
key: string
request: {
contentType?: string
description: string
acceptHeader: string
bodyParameters?: any
parameters?: Record<string, any>
}
}
interface ResponseExample {
key: string
response: {
statusCode: string
contentType?: string
description: string
example?: any
schema?: any
}
}
interface MergedExample {
key: string
request: {
contentType?: string
description: string
acceptHeader: string
bodyParameters?: any
parameters?: Record<string, any>
}
response?: {
statusCode: string
contentType?: string
description: string
example?: any
schema?: any
}
}
// Retrieves request and response examples and attempts to // Retrieves request and response examples and attempts to
// merge them to create matching request/response examples // merge them to create matching request/response examples
// The key used in the media type `examples` property is // The key used in the media type `examples` property is
// used to match requests to responses. // used to match requests to responses.
export default async function getCodeSamples(operation) { export default async function getCodeSamples(operation: Operation): Promise<MergedExample[]> {
const responseExamples = getResponseExamples(operation) const responseExamples = getResponseExamples(operation)
const requestExamples = getRequestExamples(operation) const requestExamples = getRequestExamples(operation)
@@ -18,7 +61,7 @@ export default async function getCodeSamples(operation) {
// If there are multiple examples and if the request body // If there are multiple examples and if the request body
// has the same description, add a number to the example // has the same description, add a number to the example
if (mergedExamples.length > 1) { if (mergedExamples.length > 1) {
const count = {} const count: Record<string, number> = {}
mergedExamples.forEach((item) => { mergedExamples.forEach((item) => {
count[item.request.description] = (count[item.request.description] || 0) + 1 count[item.request.description] = (count[item.request.description] || 0) + 1
}) })
@@ -33,7 +76,7 @@ export default async function getCodeSamples(operation) {
' ' + ' ' +
(i + 1) + (i + 1) +
': Status Code ' + ': Status Code ' +
example.response.statusCode example.response!.statusCode
: example.request.description, : example.request.description,
}, },
})) }))
@@ -44,7 +87,10 @@ export default async function getCodeSamples(operation) {
return mergedExamples return mergedExamples
} }
export function mergeExamples(requestExamples, responseExamples) { export function mergeExamples(
requestExamples: RequestExample[],
responseExamples: ResponseExample[],
): MergedExample[] {
// There is always at least one request example, but it won't create // There is always at least one request example, but it won't create
// a meaningful example unless it has a response example. // a meaningful example unless it has a response example.
if (requestExamples.length === 1 && responseExamples.length === 0) { if (requestExamples.length === 1 && responseExamples.length === 0) {
@@ -97,14 +143,17 @@ export function mergeExamples(requestExamples, responseExamples) {
// Iterates over the larger array or "target" (or if equal requests) to see // Iterates over the larger array or "target" (or if equal requests) to see
// if there are any matches in the smaller array or "source" // if there are any matches in the smaller array or "source"
// (or if equal responses) that can be added to target array. If a request // (or if equal responses) that can be added to target array. If a request
// If a request
// example and response example have matching keys they will be merged into // example and response example have matching keys they will be merged into
// an example. If there is more than one key match, the first match will // an example. If there is more than one key match, the first match will
// be used. // be used.
return target.filter((targetEx) => { return target
.filter((targetEx) => {
const match = source.find((srcEx) => srcEx.key === targetEx.key) const match = source.find((srcEx) => srcEx.key === targetEx.key)
if (match) return Object.assign(targetEx, match) if (match) return Object.assign(targetEx, match)
return false return false
}) })
.map((ex) => ex as MergedExample)
} }
/* /*
@@ -124,8 +173,8 @@ export function mergeExamples(requestExamples, responseExamples) {
} }
} }
*/ */
export function getRequestExamples(operation) { export function getRequestExamples(operation: Operation): RequestExample[] {
const requestExamples = [] const requestExamples: RequestExample[] = []
const parameterExamples = getParameterExamples(operation) const parameterExamples = getParameterExamples(operation)
// When no request body or parameters are defined, we create a generic // When no request body or parameters are defined, we create a generic
@@ -160,7 +209,7 @@ export function getRequestExamples(operation) {
// Requests can have multiple content types each with their own set of // Requests can have multiple content types each with their own set of
// examples. // examples.
Object.keys(operation.requestBody.content).forEach((contentType) => { Object.keys(operation.requestBody.content).forEach((contentType) => {
let examples = {} let examples: Record<string, any> = {}
// This is a fallback to allow using the `example` property in // This is a fallback to allow using the `example` property in
// the schema. If we start to enforce using examples vs. example using // the schema. If we start to enforce using examples vs. example using
// a linter, we can remove the check for `example`. // a linter, we can remove the check for `example`.
@@ -232,8 +281,8 @@ export function getRequestExamples(operation) {
} }
} }
*/ */
export function getResponseExamples(operation) { export function getResponseExamples(operation: Operation): ResponseExample[] {
const responseExamples = [] const responseExamples: ResponseExample[] = []
Object.keys(operation.responses).forEach((statusCode) => { Object.keys(operation.responses).forEach((statusCode) => {
// We don't want to create examples for error codes // We don't want to create examples for error codes
// Error codes are displayed in the status table in the docs // Error codes are displayed in the status table in the docs
@@ -259,7 +308,7 @@ export function getResponseExamples(operation) {
// Responses can have multiple content types each with their own set of // Responses can have multiple content types each with their own set of
// examples. // examples.
Object.keys(content).forEach((contentType) => { Object.keys(content).forEach((contentType) => {
let examples = {} let examples: Record<string, any> = {}
// This is a fallback to allow using the `example` property in // This is a fallback to allow using the `example` property in
// the schema. If we start to enforce using examples vs. example using // the schema. If we start to enforce using examples vs. example using
// a linter, we can remove the check for `example`. // a linter, we can remove the check for `example`.
@@ -332,13 +381,13 @@ export function getResponseExamples(operation) {
} }
} }
*/ */
export function getParameterExamples(operation) { export function getParameterExamples(operation: Operation): Record<string, Record<string, any>> {
if (!operation.parameters) { if (!operation.parameters) {
return {} return {}
} }
const parameters = operation.parameters.filter((param) => param.in === 'path') const parameters = operation.parameters.filter((param: any) => param.in === 'path')
const parameterExamples = {} const parameterExamples: Record<string, Record<string, any>> = {}
parameters.forEach((parameter) => { parameters.forEach((parameter: any) => {
const examples = parameter.examples const examples = parameter.examples
// If there are no examples, create an example from the uppercase parameter // If there are no examples, create an example from the uppercase parameter
// name, so that it is more visible that the value is fake data // name, so that it is more visible that the value is fake data

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from 'vitest' import { describe, expect, test } from 'vitest'
import { getShellExample, getGHExample, getJSExample } from '../components/get-rest-code-samples' import { getShellExample, getGHExample, getJSExample } from '../components/get-rest-code-samples'
import type { Operation } from '../components/types'
// Mock version data similar to what's used in the actual app // Mock version data similar to what's used in the actual app
const mockVersions = { const mockVersions = {
@@ -27,14 +28,19 @@ const mockVersions = {
} }
// Mock operation with standard authentication requirements // Mock operation with standard authentication requirements
const standardOperation = { const standardOperation: Operation = {
verb: 'post', verb: 'post',
title: 'Create an issue', title: 'Create an issue',
requestPath: '/repos/{owner}/{repo}/issues', requestPath: '/repos/{owner}/{repo}/issues',
serverUrl: 'https://api.github.com', serverUrl: 'https://api.github.com',
category: 'issues', category: 'issues',
subcategory: 'issues', subcategory: 'issues',
descriptionHTML: '',
previews: [],
statusCodes: [],
parameters: [], parameters: [],
bodyParameters: [],
codeExamples: [],
progAccess: { progAccess: {
userToServerRest: true, userToServerRest: true,
serverToServer: true, serverToServer: true,
@@ -45,14 +51,19 @@ const standardOperation = {
} }
// Mock operation with allowPermissionlessAccess (like revoke credentials) // Mock operation with allowPermissionlessAccess (like revoke credentials)
const unauthenticatedOperation = { const unauthenticatedOperation: Operation = {
verb: 'post', verb: 'post',
title: 'Revoke a list of credentials', title: 'Revoke a list of credentials',
requestPath: '/credentials/revoke', requestPath: '/credentials/revoke',
serverUrl: 'https://api.github.com', serverUrl: 'https://api.github.com',
category: 'credentials', category: 'credentials',
subcategory: 'revoke', subcategory: 'revoke',
descriptionHTML: '',
previews: [],
statusCodes: [],
parameters: [], parameters: [],
bodyParameters: [],
codeExamples: [],
progAccess: { progAccess: {
userToServerRest: true, userToServerRest: true,
serverToServer: true, serverToServer: true,
@@ -63,14 +74,19 @@ const unauthenticatedOperation = {
} }
// Mock operation with basic auth (like OAuth apps) // Mock operation with basic auth (like OAuth apps)
const basicAuthOperation = { const basicAuthOperation: Operation = {
verb: 'post', verb: 'post',
title: 'Create an OAuth app', title: 'Create an OAuth app',
requestPath: '/orgs/{org}/oauth/apps', requestPath: '/orgs/{org}/oauth/apps',
serverUrl: 'https://api.github.com', serverUrl: 'https://api.github.com',
category: 'apps', category: 'apps',
subcategory: 'oauth-applications', subcategory: 'oauth-applications',
descriptionHTML: '',
previews: [],
statusCodes: [],
parameters: [], parameters: [],
bodyParameters: [],
codeExamples: [],
progAccess: { progAccess: {
userToServerRest: true, userToServerRest: true,
serverToServer: false, serverToServer: false,
@@ -81,14 +97,19 @@ const basicAuthOperation = {
} }
// Mock operation for GHES manage API // Mock operation for GHES manage API
const ghesManageOperation = { const ghesManageOperation: Operation = {
verb: 'post', verb: 'post',
title: 'Set maintenance mode', title: 'Set maintenance mode',
requestPath: '/setup/api/maintenance', requestPath: '/setup/api/maintenance',
serverUrl: 'https://HOSTNAME', serverUrl: 'https://HOSTNAME',
category: 'enterprise-admin', category: 'enterprise-admin',
subcategory: 'manage-ghes', subcategory: 'manage-ghes',
descriptionHTML: '',
previews: [],
statusCodes: [],
parameters: [], parameters: [],
bodyParameters: [],
codeExamples: [],
progAccess: { progAccess: {
userToServerRest: true, userToServerRest: true,
serverToServer: false, serverToServer: false,
@@ -98,7 +119,7 @@ const ghesManageOperation = {
} }
// Mock code sample // Mock code sample
const mockCodeSample = { const mockCodeSample: any = {
key: 'default', key: 'default',
request: { request: {
contentType: 'application/json', contentType: 'application/json',
@@ -117,7 +138,7 @@ const mockCodeSample = {
}, },
} }
const mockCodeSampleWithoutBody = { const mockCodeSampleWithoutBody: any = {
key: 'default', key: 'default',
request: { request: {
contentType: 'application/json', contentType: 'application/json',
@@ -141,7 +162,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -152,7 +173,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'enterprise-cloud@latest', 'enterprise-cloud@latest',
mockVersions, mockVersions as any,
) )
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -163,7 +184,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'enterprise-cloud@2024-01-01', 'enterprise-cloud@2024-01-01',
mockVersions, mockVersions as any,
) )
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -174,7 +195,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'enterprise-server@3.17', 'enterprise-server@3.17',
mockVersions, mockVersions as any,
) )
expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -185,7 +206,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'github-ae@latest', 'github-ae@latest',
mockVersions, mockVersions as any,
) )
expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -198,7 +219,7 @@ describe('REST code samples authentication header handling', () => {
standardOperation, standardOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -211,7 +232,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -225,7 +246,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'enterprise-cloud@latest', 'enterprise-cloud@latest',
mockVersions, mockVersions as any,
) )
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -238,7 +259,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'enterprise-server@3.17', 'enterprise-server@3.17',
mockVersions, mockVersions as any,
) )
expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -251,7 +272,7 @@ describe('REST code samples authentication header handling', () => {
basicAuthOperation, basicAuthOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).toContain('-u "<YOUR_CLIENT_ID>:<YOUR_CLIENT_SECRET>"') expect(result).toContain('-u "<YOUR_CLIENT_ID>:<YOUR_CLIENT_SECRET>"')
@@ -263,7 +284,7 @@ describe('REST code samples authentication header handling', () => {
ghesManageOperation, ghesManageOperation,
mockCodeSample, mockCodeSample,
'enterprise-server@3.17', 'enterprise-server@3.17',
mockVersions, mockVersions as any,
) )
expect(result).toContain('-u "api_key:your-password"') expect(result).toContain('-u "api_key:your-password"')
@@ -273,10 +294,10 @@ describe('REST code samples authentication header handling', () => {
test('handles GET requests without body parameters correctly', () => { test('handles GET requests without body parameters correctly', () => {
const getOperation = { ...unauthenticatedOperation, verb: 'get' } const getOperation = { ...unauthenticatedOperation, verb: 'get' }
const result = getShellExample( const result = getShellExample(
getOperation, getOperation as any,
mockCodeSampleWithoutBody, mockCodeSampleWithoutBody,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -291,7 +312,7 @@ describe('REST code samples authentication header handling', () => {
standardOperation, standardOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).toContain('gh api') expect(result).toContain('gh api')
@@ -306,7 +327,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).toContain('gh api') expect(result).toContain('gh api')
@@ -322,7 +343,7 @@ describe('REST code samples authentication header handling', () => {
basicAuthOperation, basicAuthOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).toBeUndefined() expect(result).toBeUndefined()
@@ -330,7 +351,12 @@ describe('REST code samples authentication header handling', () => {
test('generates example for GHES with hostname parameter', () => { test('generates example for GHES with hostname parameter', () => {
const ghesOp = { ...standardOperation, serverUrl: 'https://github.example.com' } const ghesOp = { ...standardOperation, serverUrl: 'https://github.example.com' }
const result = getGHExample(ghesOp, mockCodeSample, 'enterprise-server@3.17', mockVersions) const result = getGHExample(
ghesOp,
mockCodeSample,
'enterprise-server@3.17',
mockVersions as any,
)
expect(result).toContain('--hostname HOSTNAME') expect(result).toContain('--hostname HOSTNAME')
}) })
@@ -342,7 +368,7 @@ describe('REST code samples authentication header handling', () => {
standardOperation, standardOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).toContain("auth: 'YOUR-TOKEN'") expect(result).toContain("auth: 'YOUR-TOKEN'")
@@ -355,7 +381,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).not.toContain('const octokit = new Octokit({\n "auth": "YOUR-TOKEN"\n})') expect(result).not.toContain('const octokit = new Octokit({\n "auth": "YOUR-TOKEN"\n})')
@@ -369,7 +395,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'enterprise-cloud@latest', 'enterprise-cloud@latest',
mockVersions, mockVersions as any,
) )
expect(result).not.toContain('const octokit = new Octokit({\n "auth": "YOUR-TOKEN"\n})') expect(result).not.toContain('const octokit = new Octokit({\n "auth": "YOUR-TOKEN"\n})')
@@ -382,7 +408,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSample, mockCodeSample,
'enterprise-server@3.17', 'enterprise-server@3.17',
mockVersions, mockVersions as any,
) )
expect(result).toContain("auth: 'YOUR-TOKEN'") expect(result).toContain("auth: 'YOUR-TOKEN'")
@@ -394,7 +420,7 @@ describe('REST code samples authentication header handling', () => {
basicAuthOperation, basicAuthOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).toContain('import { createOAuthAppAuth } from "@octokit/auth-oauth-app"') expect(result).toContain('import { createOAuthAppAuth } from "@octokit/auth-oauth-app"')
@@ -408,7 +434,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
mockCodeSampleWithoutBody, mockCodeSampleWithoutBody,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).toContain('const octokit = new Octokit()') expect(result).toContain('const octokit = new Octokit()')
@@ -426,10 +452,10 @@ describe('REST code samples authentication header handling', () => {
} }
const shellResult = getShellExample( const shellResult = getShellExample(
operationWithoutProgAccess, operationWithoutProgAccess as any,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
// Should default to including authentication when progAccess is undefined // Should default to including authentication when progAccess is undefined
@@ -453,7 +479,7 @@ describe('REST code samples authentication header handling', () => {
operationWithoutProperty, operationWithoutProperty,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
// Should default to including authentication when property is missing // Should default to including authentication when property is missing
@@ -473,7 +499,7 @@ describe('REST code samples authentication header handling', () => {
unauthenticatedOperation, unauthenticatedOperation,
nullSample, nullSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"') expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
@@ -490,7 +516,7 @@ describe('REST code samples authentication header handling', () => {
mixedAuthOperation, mixedAuthOperation,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
// Should still use management console auth even for allowPermissionlessAccess operations // Should still use management console auth even for allowPermissionlessAccess operations
@@ -509,7 +535,7 @@ describe('REST code samples authentication header handling', () => {
enterpriseUnauthOp, enterpriseUnauthOp,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(enterpriseResult).toContain('-u "api_key:your-password"') expect(enterpriseResult).toContain('-u "api_key:your-password"')
@@ -528,7 +554,7 @@ describe('REST code samples authentication header handling', () => {
basicAuthUnauthOp, basicAuthUnauthOp,
mockCodeSample, mockCodeSample,
'free-pro-team@latest', 'free-pro-team@latest',
mockVersions, mockVersions as any,
) )
expect(basicAuthResult).toContain('-u "<YOUR_CLIENT_ID>:<YOUR_CLIENT_SECRET>"') expect(basicAuthResult).toContain('-u "<YOUR_CLIENT_ID>:<YOUR_CLIENT_SECRET>"')