Convert 6 files to TypeScript (#58003)
This commit is contained in:
@@ -1,9 +1,38 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
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', () => {
|
||||
// Mock data structure representing operations with metadata permissions
|
||||
const mockOperationsWithMetadata = [
|
||||
const mockOperationsWithMetadata: Operation[] = [
|
||||
{
|
||||
operationId: 'repos/enable-automated-security-fixes',
|
||||
permissionSets: [{ metadata: 'read', administration: 'write' }],
|
||||
@@ -23,7 +52,7 @@ describe('metadata permissions filtering', () => {
|
||||
]
|
||||
|
||||
// Mock programmatic access data
|
||||
const mockProgAccessData = {
|
||||
const mockProgAccessData: Record<string, ProgAccessData> = {
|
||||
'repos/enable-automated-security-fixes': {
|
||||
userToServerRest: true,
|
||||
serverToServer: true,
|
||||
@@ -47,7 +76,7 @@ describe('metadata permissions filtering', () => {
|
||||
}
|
||||
|
||||
// Mock actor resources
|
||||
const mockProgActorResources = {
|
||||
const mockProgActorResources: Record<string, ActorResource> = {
|
||||
metadata: {
|
||||
title: 'Metadata',
|
||||
visibility: 'public',
|
||||
@@ -95,8 +124,8 @@ describe('metadata permissions filtering', () => {
|
||||
})
|
||||
|
||||
test('filters metadata operations with additional permissions', () => {
|
||||
const filteredOperations = []
|
||||
const metadataPermissions = []
|
||||
const filteredOperations: FilteredOperation[] = []
|
||||
const metadataPermissions: MetadataPermission[] = []
|
||||
|
||||
for (const operation of mockOperationsWithMetadata) {
|
||||
const progData = mockProgAccessData[operation.operationId]
|
||||
@@ -137,15 +166,15 @@ describe('metadata permissions filtering', () => {
|
||||
// Should have other permissions from operations with additional permissions
|
||||
const adminPermission = filteredOperations.find((op) => op.permission === 'administration')
|
||||
expect(adminPermission).toBeDefined()
|
||||
expect(adminPermission.operationId).toBe('repos/enable-automated-security-fixes')
|
||||
expect(adminPermission.additionalPermissions).toBe(true)
|
||||
expect(adminPermission!.operationId).toBe('repos/enable-automated-security-fixes')
|
||||
expect(adminPermission!.additionalPermissions).toBe(true)
|
||||
|
||||
const orgAdminPermission = filteredOperations.find(
|
||||
(op) => op.permission === 'organization_administration',
|
||||
)
|
||||
expect(orgAdminPermission).toBeDefined()
|
||||
expect(orgAdminPermission.operationId).toBe('orgs/update-webhook')
|
||||
expect(orgAdminPermission.additionalPermissions).toBe(true)
|
||||
expect(orgAdminPermission!.operationId).toBe('orgs/update-webhook')
|
||||
expect(orgAdminPermission!.additionalPermissions).toBe(true)
|
||||
})
|
||||
|
||||
test('preserves non-metadata permissions regardless of additional permissions', () => {
|
||||
@@ -168,11 +197,11 @@ describe('metadata permissions filtering', () => {
|
||||
expect(shouldFilterMetadataPermission('metadata', [])).toBe(false)
|
||||
|
||||
// Permission set with empty object (edge case)
|
||||
const edgeCase1 = [{ metadata: 'read' }, {}]
|
||||
const edgeCase1: Record<string, string>[] = [{ metadata: 'read' }, {}]
|
||||
expect(shouldFilterMetadataPermission('metadata', edgeCase1)).toBe(true)
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
@@ -207,17 +236,23 @@ describe('metadata permissions filtering', () => {
|
||||
|
||||
test('handles complex permission structures from real data', () => {
|
||||
// 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)
|
||||
|
||||
// Single permission set with multiple permissions (should filter metadata)
|
||||
const multiplePermissionsInSet = [
|
||||
const multiplePermissionsInSet: Record<string, string>[] = [
|
||||
{ metadata: 'read', contents: 'write', pull_requests: 'write' },
|
||||
]
|
||||
expect(shouldFilterMetadataPermission('metadata', multiplePermissionsInSet)).toBe(true)
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
@@ -250,7 +285,7 @@ describe('metadata permissions filtering', () => {
|
||||
})
|
||||
|
||||
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
|
||||
{ permissionSets: [{ metadata: 'read' }], expected: false },
|
||||
|
||||
@@ -283,12 +318,7 @@ describe('metadata permissions filtering', () => {
|
||||
// - DELETE /orgs/{org}/actions/permissions/repositories/{repository_id}
|
||||
// Because they have metadata + organization_administration permissions
|
||||
|
||||
const mockMutatingOperation = {
|
||||
operationId: 'actions/set-selected-repositories-enabled-github-actions-organization',
|
||||
permissionSets: [{ metadata: 'read', organization_administration: 'write' }],
|
||||
}
|
||||
|
||||
const progData = {
|
||||
const progData: ProgAccessData = {
|
||||
userToServerRest: true,
|
||||
serverToServer: true,
|
||||
permissions: [{ metadata: 'read', organization_administration: 'write' }],
|
||||
@@ -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 fs from 'fs'
|
||||
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
|
||||
* 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
|
||||
// and tag the changelog entry with it
|
||||
const todayString = new Date().toISOString().slice(0, 10)
|
||||
changelogEntry.date = todayString
|
||||
|
||||
const previousChangelogString = fs.readFileSync(targetPath)
|
||||
const previousChangelog = JSON.parse(previousChangelogString)
|
||||
const previousChangelogString = fs.readFileSync(targetPath, 'utf8')
|
||||
const previousChangelog = JSON.parse(previousChangelogString) as ChangelogEntry[]
|
||||
// add a new entry to the changelog data
|
||||
previousChangelog.unshift(changelogEntry)
|
||||
// rewrite the updated changelog
|
||||
@@ -29,28 +80,23 @@ export function prependDatedEntry(changelogEntry, targetPath) {
|
||||
* changes that warrant a changelog entry, return a changelog entry.
|
||||
* Based on the parsed `previews`, identify changes that are under a preview.
|
||||
* 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(
|
||||
oldSchemaString,
|
||||
newSchemaString,
|
||||
previews,
|
||||
oldUpcomingChanges,
|
||||
newUpcomingChanges,
|
||||
) {
|
||||
oldSchemaString: string,
|
||||
newSchemaString: string,
|
||||
previews: Preview[],
|
||||
oldUpcomingChanges: UpcomingChange[],
|
||||
newUpcomingChanges: UpcomingChange[],
|
||||
): Promise<ChangelogEntry | null> {
|
||||
// Create schema objects out of the strings
|
||||
const oldSchema = await loadSchema(oldSchemaString, {})
|
||||
const newSchema = await loadSchema(newSchemaString, {})
|
||||
// Using 'as any' because loadSchema accepts string schema directly without requiring loaders
|
||||
const oldSchema = await loadSchema(oldSchemaString, {} as any)
|
||||
const newSchema = await loadSchema(newSchemaString, {} as any)
|
||||
|
||||
// Generate changes between the two schemas
|
||||
const changes = await diff(oldSchema, newSchema)
|
||||
const changesToReport = []
|
||||
const ignoredChanges = []
|
||||
const changesToReport: Change[] = []
|
||||
const ignoredChanges: Change[] = []
|
||||
changes.forEach((change) => {
|
||||
if (CHANGES_TO_REPORT.includes(change.type)) {
|
||||
changesToReport.push(change)
|
||||
@@ -76,14 +122,14 @@ export async function createChangelogEntry(
|
||||
}
|
||||
|
||||
// Store ignored changes for potential workflow outputs
|
||||
createChangelogEntry.lastIgnoredChanges = ignoredChanges
|
||||
;(createChangelogEntry as any).lastIgnoredChanges = ignoredChanges
|
||||
|
||||
const { schemaChangesToReport, previewChangesToReport } = segmentPreviewChanges(
|
||||
changesToReport,
|
||||
previews,
|
||||
)
|
||||
|
||||
const addedUpcomingChanges = newUpcomingChanges.filter(function (change) {
|
||||
const addedUpcomingChanges = newUpcomingChanges.filter(function (change): boolean {
|
||||
// Manually check each of `newUpcomingChanges` for an equivalent entry
|
||||
// in `oldUpcomingChanges`.
|
||||
return !oldUpcomingChanges.find(function (oldChange) {
|
||||
@@ -98,10 +144,10 @@ export async function createChangelogEntry(
|
||||
// If there were any changes, create a changelog entry
|
||||
if (
|
||||
schemaChangesToReport.length > 0 ||
|
||||
previewChangesToReport.length > 0 ||
|
||||
Object.keys(previewChangesToReport).length > 0 ||
|
||||
addedUpcomingChanges.length > 0
|
||||
) {
|
||||
const changelogEntry = {
|
||||
const changelogEntry: ChangelogEntry = {
|
||||
schemaChanges: [],
|
||||
previewChanges: [],
|
||||
upcomingChanges: [],
|
||||
@@ -109,11 +155,11 @@ export async function createChangelogEntry(
|
||||
|
||||
const cleanedSchemaChanges = cleanMessagesFromChanges(schemaChangesToReport)
|
||||
const renderedScheamChanges = await Promise.all(
|
||||
cleanedSchemaChanges.map(async (change) => {
|
||||
cleanedSchemaChanges.map(async (change): Promise<string> => {
|
||||
return await renderContent(change)
|
||||
}),
|
||||
)
|
||||
const schemaChange = {
|
||||
const schemaChange: ChangelogSchemaChange = {
|
||||
title: 'The GraphQL schema includes these changes:',
|
||||
// Replace single quotes which wrap field/argument/type names with backticks
|
||||
changes: renderedScheamChanges,
|
||||
@@ -124,7 +170,7 @@ export async function createChangelogEntry(
|
||||
const previewChanges = previewChangesToReport[previewTitle]
|
||||
const cleanedPreviewChanges = cleanMessagesFromChanges(previewChanges.changes)
|
||||
const renderedPreviewChanges = await Promise.all(
|
||||
cleanedPreviewChanges.map(async (change) => {
|
||||
cleanedPreviewChanges.map(async (change): Promise<string> => {
|
||||
return renderContent(change)
|
||||
}),
|
||||
)
|
||||
@@ -146,10 +192,10 @@ export async function createChangelogEntry(
|
||||
const location = change.location
|
||||
const description = change.description
|
||||
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(
|
||||
cleanedUpcomingChanges.map(async (change) => {
|
||||
cleanedUpcomingChanges.map(async (change): Promise<string> => {
|
||||
return await renderContent(change)
|
||||
}),
|
||||
)
|
||||
@@ -167,10 +213,8 @@ export async function createChangelogEntry(
|
||||
|
||||
/**
|
||||
* 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') {
|
||||
title = 'Update refs preview'
|
||||
} else if (title === 'MergeInfoPreview') {
|
||||
@@ -184,10 +228,8 @@ export function cleanPreviewTitle(title) {
|
||||
/**
|
||||
* Turn the given title into an HTML-ready anchor.
|
||||
* (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
|
||||
.toLowerCase()
|
||||
.replace(/ /g, '-')
|
||||
@@ -196,11 +238,9 @@ export function previewAnchor(previewTitle) {
|
||||
|
||||
/**
|
||||
* Turn changes from graphql-inspector into messages for the HTML changelog.
|
||||
* @param {Array<object>} changes
|
||||
* @return {Array<string>}
|
||||
*/
|
||||
export function cleanMessagesFromChanges(changes) {
|
||||
return changes.map(function (change) {
|
||||
export function cleanMessagesFromChanges(changes: Change[]): string[] {
|
||||
return changes.map(function (change): string {
|
||||
// replace single quotes around graphql names with backticks,
|
||||
// to match previous behavior from graphql-schema-comparator
|
||||
return change.message.replace(/'([a-zA-Z. :!]+)'/g, '`$1`')
|
||||
@@ -212,29 +252,29 @@ export function cleanMessagesFromChanges(changes) {
|
||||
* one for changes in the main schema,
|
||||
* and another for changes that are under preview.
|
||||
* (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` }
|
||||
// for easier lookup of change to preview
|
||||
const pathToPreview = {}
|
||||
previews.forEach(function (preview) {
|
||||
const pathToPreview: Record<string, string> = {}
|
||||
previews.forEach(function (preview): void {
|
||||
preview.toggled_on.forEach(function (path) {
|
||||
pathToPreview[path] = preview.title
|
||||
})
|
||||
})
|
||||
const schemaChanges = []
|
||||
const changesByPreview = {}
|
||||
const schemaChanges: Change[] = []
|
||||
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
|
||||
// is covered by a preview. If it is, mark this change as belonging to a preview
|
||||
const pathParts = change.path.split('.')
|
||||
let testPath = null
|
||||
let previewTitle = null
|
||||
let previewChanges = null
|
||||
const pathParts = change.path?.split('.') || []
|
||||
let testPath: string | null = null
|
||||
let previewTitle: string | null = null
|
||||
let previewChanges: PreviewChanges | null = null
|
||||
while (pathParts.length > 0 && !previewTitle) {
|
||||
testPath = pathParts.join('.')
|
||||
previewTitle = pathToPreview[testPath]
|
||||
@@ -294,28 +334,28 @@ const CHANGES_TO_REPORT = [
|
||||
|
||||
/**
|
||||
* Get the ignored change types from the last changelog entry creation
|
||||
* @returns {Array} Array of ignored change objects
|
||||
*/
|
||||
export function getLastIgnoredChanges() {
|
||||
return createChangelogEntry.lastIgnoredChanges || []
|
||||
export function getLastIgnoredChanges(): Change[] {
|
||||
return (createChangelogEntry as any).lastIgnoredChanges || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
if (ignored.length === 0) return null
|
||||
|
||||
const types = [...new Set(ignored.map((change) => change.type))]
|
||||
const summary = {
|
||||
const summary: IgnoredChangesSummary = {
|
||||
totalCount: ignored.length,
|
||||
typeCount: types.length,
|
||||
types: types.map((type) => ({
|
||||
types: types.map(
|
||||
(type): IgnoredChangeType => ({
|
||||
type,
|
||||
count: ignored.filter((change) => change.type === type).length,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
return summary
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
prependDatedEntry,
|
||||
getLastIgnoredChanges,
|
||||
getIgnoredChangesSummary,
|
||||
type ChangelogEntry,
|
||||
} from '../scripts/build-changelog'
|
||||
import readJsonFile from '@/frame/lib/read-json-file'
|
||||
|
||||
@@ -30,10 +31,6 @@ interface UpcomingChange {
|
||||
date: string
|
||||
}
|
||||
|
||||
interface ChangelogEntry {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface IgnoredChange {
|
||||
type: string
|
||||
[key: string]: any
|
||||
@@ -245,7 +242,11 @@ describe('updating the changelog file', () => {
|
||||
const testTargetPath = 'src/graphql/tests/fixtures/example-changelog.json'
|
||||
const previousContents = await fs.readFile(testTargetPath)
|
||||
|
||||
const exampleEntry: ChangelogEntry = { someStuff: true }
|
||||
const exampleEntry: ChangelogEntry = {
|
||||
schemaChanges: [],
|
||||
previewChanges: [],
|
||||
upcomingChanges: [],
|
||||
}
|
||||
const expectedDate = '2020-11-20'
|
||||
MockDate.set(expectedDate)
|
||||
|
||||
@@ -254,7 +255,12 @@ describe('updating the changelog file', () => {
|
||||
// reset the file:
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[
|
||||
{
|
||||
"someStuff": true,
|
||||
"date": "2020-11-20"
|
||||
"date": "2020-11-20",
|
||||
"schemaChanges": [],
|
||||
"previewChanges": [],
|
||||
"upcomingChanges": []
|
||||
},
|
||||
{
|
||||
"previous_entry": "..."
|
||||
|
||||
@@ -5,11 +5,54 @@ const DEFAULT_EXAMPLE_DESCRIPTION = 'Example'
|
||||
const DEFAULT_EXAMPLE_KEY = 'default'
|
||||
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
|
||||
// merge them to create matching request/response examples
|
||||
// The key used in the media type `examples` property is
|
||||
// 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 requestExamples = getRequestExamples(operation)
|
||||
|
||||
@@ -18,7 +61,7 @@ export default async function getCodeSamples(operation) {
|
||||
// If there are multiple examples and if the request body
|
||||
// has the same description, add a number to the example
|
||||
if (mergedExamples.length > 1) {
|
||||
const count = {}
|
||||
const count: Record<string, number> = {}
|
||||
mergedExamples.forEach((item) => {
|
||||
count[item.request.description] = (count[item.request.description] || 0) + 1
|
||||
})
|
||||
@@ -33,7 +76,7 @@ export default async function getCodeSamples(operation) {
|
||||
' ' +
|
||||
(i + 1) +
|
||||
': Status Code ' +
|
||||
example.response.statusCode
|
||||
example.response!.statusCode
|
||||
: example.request.description,
|
||||
},
|
||||
}))
|
||||
@@ -44,7 +87,10 @@ export default async function getCodeSamples(operation) {
|
||||
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
|
||||
// a meaningful example unless it has a response example.
|
||||
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
|
||||
// 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
|
||||
// If a request
|
||||
// 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
|
||||
// be used.
|
||||
return target.filter((targetEx) => {
|
||||
return target
|
||||
.filter((targetEx) => {
|
||||
const match = source.find((srcEx) => srcEx.key === targetEx.key)
|
||||
if (match) return Object.assign(targetEx, match)
|
||||
return false
|
||||
})
|
||||
.map((ex) => ex as MergedExample)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -124,8 +173,8 @@ export function mergeExamples(requestExamples, responseExamples) {
|
||||
}
|
||||
}
|
||||
*/
|
||||
export function getRequestExamples(operation) {
|
||||
const requestExamples = []
|
||||
export function getRequestExamples(operation: Operation): RequestExample[] {
|
||||
const requestExamples: RequestExample[] = []
|
||||
const parameterExamples = getParameterExamples(operation)
|
||||
|
||||
// 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
|
||||
// examples.
|
||||
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
|
||||
// the schema. If we start to enforce using examples vs. example using
|
||||
// a linter, we can remove the check for `example`.
|
||||
@@ -232,8 +281,8 @@ export function getRequestExamples(operation) {
|
||||
}
|
||||
}
|
||||
*/
|
||||
export function getResponseExamples(operation) {
|
||||
const responseExamples = []
|
||||
export function getResponseExamples(operation: Operation): ResponseExample[] {
|
||||
const responseExamples: ResponseExample[] = []
|
||||
Object.keys(operation.responses).forEach((statusCode) => {
|
||||
// We don't want to create examples for error codes
|
||||
// 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
|
||||
// examples.
|
||||
Object.keys(content).forEach((contentType) => {
|
||||
let examples = {}
|
||||
let examples: Record<string, any> = {}
|
||||
// This is a fallback to allow using the `example` property in
|
||||
// the schema. If we start to enforce using examples vs. example using
|
||||
// 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) {
|
||||
return {}
|
||||
}
|
||||
const parameters = operation.parameters.filter((param) => param.in === 'path')
|
||||
const parameterExamples = {}
|
||||
parameters.forEach((parameter) => {
|
||||
const parameters = operation.parameters.filter((param: any) => param.in === 'path')
|
||||
const parameterExamples: Record<string, Record<string, any>> = {}
|
||||
parameters.forEach((parameter: any) => {
|
||||
const examples = parameter.examples
|
||||
// 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
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
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
|
||||
const mockVersions = {
|
||||
@@ -27,14 +28,19 @@ const mockVersions = {
|
||||
}
|
||||
|
||||
// Mock operation with standard authentication requirements
|
||||
const standardOperation = {
|
||||
const standardOperation: Operation = {
|
||||
verb: 'post',
|
||||
title: 'Create an issue',
|
||||
requestPath: '/repos/{owner}/{repo}/issues',
|
||||
serverUrl: 'https://api.github.com',
|
||||
category: 'issues',
|
||||
subcategory: 'issues',
|
||||
descriptionHTML: '',
|
||||
previews: [],
|
||||
statusCodes: [],
|
||||
parameters: [],
|
||||
bodyParameters: [],
|
||||
codeExamples: [],
|
||||
progAccess: {
|
||||
userToServerRest: true,
|
||||
serverToServer: true,
|
||||
@@ -45,14 +51,19 @@ const standardOperation = {
|
||||
}
|
||||
|
||||
// Mock operation with allowPermissionlessAccess (like revoke credentials)
|
||||
const unauthenticatedOperation = {
|
||||
const unauthenticatedOperation: Operation = {
|
||||
verb: 'post',
|
||||
title: 'Revoke a list of credentials',
|
||||
requestPath: '/credentials/revoke',
|
||||
serverUrl: 'https://api.github.com',
|
||||
category: 'credentials',
|
||||
subcategory: 'revoke',
|
||||
descriptionHTML: '',
|
||||
previews: [],
|
||||
statusCodes: [],
|
||||
parameters: [],
|
||||
bodyParameters: [],
|
||||
codeExamples: [],
|
||||
progAccess: {
|
||||
userToServerRest: true,
|
||||
serverToServer: true,
|
||||
@@ -63,14 +74,19 @@ const unauthenticatedOperation = {
|
||||
}
|
||||
|
||||
// Mock operation with basic auth (like OAuth apps)
|
||||
const basicAuthOperation = {
|
||||
const basicAuthOperation: Operation = {
|
||||
verb: 'post',
|
||||
title: 'Create an OAuth app',
|
||||
requestPath: '/orgs/{org}/oauth/apps',
|
||||
serverUrl: 'https://api.github.com',
|
||||
category: 'apps',
|
||||
subcategory: 'oauth-applications',
|
||||
descriptionHTML: '',
|
||||
previews: [],
|
||||
statusCodes: [],
|
||||
parameters: [],
|
||||
bodyParameters: [],
|
||||
codeExamples: [],
|
||||
progAccess: {
|
||||
userToServerRest: true,
|
||||
serverToServer: false,
|
||||
@@ -81,14 +97,19 @@ const basicAuthOperation = {
|
||||
}
|
||||
|
||||
// Mock operation for GHES manage API
|
||||
const ghesManageOperation = {
|
||||
const ghesManageOperation: Operation = {
|
||||
verb: 'post',
|
||||
title: 'Set maintenance mode',
|
||||
requestPath: '/setup/api/maintenance',
|
||||
serverUrl: 'https://HOSTNAME',
|
||||
category: 'enterprise-admin',
|
||||
subcategory: 'manage-ghes',
|
||||
descriptionHTML: '',
|
||||
previews: [],
|
||||
statusCodes: [],
|
||||
parameters: [],
|
||||
bodyParameters: [],
|
||||
codeExamples: [],
|
||||
progAccess: {
|
||||
userToServerRest: true,
|
||||
serverToServer: false,
|
||||
@@ -98,7 +119,7 @@ const ghesManageOperation = {
|
||||
}
|
||||
|
||||
// Mock code sample
|
||||
const mockCodeSample = {
|
||||
const mockCodeSample: any = {
|
||||
key: 'default',
|
||||
request: {
|
||||
contentType: 'application/json',
|
||||
@@ -117,7 +138,7 @@ const mockCodeSample = {
|
||||
},
|
||||
}
|
||||
|
||||
const mockCodeSampleWithoutBody = {
|
||||
const mockCodeSampleWithoutBody: any = {
|
||||
key: 'default',
|
||||
request: {
|
||||
contentType: 'application/json',
|
||||
@@ -141,7 +162,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -152,7 +173,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'enterprise-cloud@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -163,7 +184,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'enterprise-cloud@2024-01-01',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -174,7 +195,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'enterprise-server@3.17',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -185,7 +206,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'github-ae@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -198,7 +219,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
standardOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -211,7 +232,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -225,7 +246,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'enterprise-cloud@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -238,7 +259,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'enterprise-server@3.17',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -251,7 +272,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
basicAuthOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain('-u "<YOUR_CLIENT_ID>:<YOUR_CLIENT_SECRET>"')
|
||||
@@ -263,7 +284,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
ghesManageOperation,
|
||||
mockCodeSample,
|
||||
'enterprise-server@3.17',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
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', () => {
|
||||
const getOperation = { ...unauthenticatedOperation, verb: 'get' }
|
||||
const result = getShellExample(
|
||||
getOperation,
|
||||
getOperation as any,
|
||||
mockCodeSampleWithoutBody,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -291,7 +312,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
standardOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain('gh api')
|
||||
@@ -306,7 +327,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain('gh api')
|
||||
@@ -322,7 +343,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
basicAuthOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
@@ -330,7 +351,12 @@ describe('REST code samples authentication header handling', () => {
|
||||
|
||||
test('generates example for GHES with hostname parameter', () => {
|
||||
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')
|
||||
})
|
||||
@@ -342,7 +368,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
standardOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain("auth: 'YOUR-TOKEN'")
|
||||
@@ -355,7 +381,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
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,
|
||||
mockCodeSample,
|
||||
'enterprise-cloud@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
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,
|
||||
mockCodeSample,
|
||||
'enterprise-server@3.17',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain("auth: 'YOUR-TOKEN'")
|
||||
@@ -394,7 +420,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
basicAuthOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain('import { createOAuthAppAuth } from "@octokit/auth-oauth-app"')
|
||||
@@ -408,7 +434,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
mockCodeSampleWithoutBody,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).toContain('const octokit = new Octokit()')
|
||||
@@ -426,10 +452,10 @@ describe('REST code samples authentication header handling', () => {
|
||||
}
|
||||
|
||||
const shellResult = getShellExample(
|
||||
operationWithoutProgAccess,
|
||||
operationWithoutProgAccess as any,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
// Should default to including authentication when progAccess is undefined
|
||||
@@ -453,7 +479,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
operationWithoutProperty,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
// Should default to including authentication when property is missing
|
||||
@@ -473,7 +499,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
unauthenticatedOperation,
|
||||
nullSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(result).not.toContain('-H "Authorization: Bearer <YOUR-TOKEN>"')
|
||||
@@ -490,7 +516,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
mixedAuthOperation,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
// Should still use management console auth even for allowPermissionlessAccess operations
|
||||
@@ -509,7 +535,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
enterpriseUnauthOp,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(enterpriseResult).toContain('-u "api_key:your-password"')
|
||||
@@ -528,7 +554,7 @@ describe('REST code samples authentication header handling', () => {
|
||||
basicAuthUnauthOp,
|
||||
mockCodeSample,
|
||||
'free-pro-team@latest',
|
||||
mockVersions,
|
||||
mockVersions as any,
|
||||
)
|
||||
|
||||
expect(basicAuthResult).toContain('-u "<YOUR_CLIENT_ID>:<YOUR_CLIENT_SECRET>"')
|
||||
Reference in New Issue
Block a user