mirror of
https://github.com/supabase/supabase.git
synced 2026-05-07 01:10:15 -04:00
cf3ecc93eb
strictNullChecks was off for docs, which lets errors slip through and leads to incorrect required/optional typing on Zod-inferred types. This PR enables strictNullChecks and fixes all the existing violations.
330 lines
10 KiB
TypeScript
330 lines
10 KiB
TypeScript
import type { TsDoc } from '../../generator/legacy/definitions'
|
|
|
|
import { mapValues, values } from 'lodash-es'
|
|
import type { OpenAPIV3 } from 'openapi-types'
|
|
import type { ICommonItem } from '~/components/reference/Reference.types'
|
|
import { flattenSections } from '../helpers'
|
|
|
|
export function extractTsDocNode(nodeToFind: string, definition: any) {
|
|
const nodePath = nodeToFind.split('.')
|
|
let i = 0
|
|
let previousNode = definition
|
|
let currentNode = definition
|
|
while (i < nodePath.length) {
|
|
previousNode = currentNode
|
|
currentNode = previousNode.children.find((x) => x.name == nodePath[i]) || null
|
|
if (currentNode == null) {
|
|
console.log(`Cant find ${nodePath[i]} in ${previousNode.children.map((x) => '\n' + x.name)}`)
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
|
|
return currentNode
|
|
}
|
|
|
|
export function generateParameters(tsDefinition: any) {
|
|
let functionDeclaration: any = null
|
|
if (tsDefinition.kindString == 'Method') {
|
|
functionDeclaration = tsDefinition
|
|
} else if (tsDefinition.kindString == 'Constructor') {
|
|
functionDeclaration = tsDefinition
|
|
} else functionDeclaration = tsDefinition?.type?.declaration
|
|
if (!functionDeclaration || !functionDeclaration.signatures) return ''
|
|
|
|
// Functions can have multiple signatures - select the last one since that
|
|
// tends to be closer to primitive types (citation needed).
|
|
const paramDefinitions: TsDoc.TypeDefinition[] = functionDeclaration.signatures.at(-1)?.parameters
|
|
if (!paramDefinitions) return ''
|
|
|
|
// const paramsComments: TsDoc.CommentTag = tsDefinition.comment?.tags?.filter(x => x.tag == 'param')
|
|
let parameters = paramDefinitions.map((x) => recurseThroughParams(x)) // old join // .join(`\n`)
|
|
return parameters
|
|
}
|
|
|
|
function recurseThroughParams(paramDefinition: any) {
|
|
const param = { ...paramDefinition }
|
|
const labelParams = generateLabelParam(param)
|
|
|
|
let children: any[] | undefined
|
|
if (param.type?.type === 'literal') {
|
|
// skip: literal types have no children
|
|
} else if (param.type?.type === 'intrinsic') {
|
|
// primitive types
|
|
if (!['string', 'number', 'boolean', 'object', 'unknown'].includes(param.type?.name)) {
|
|
// skip for now
|
|
//throw new Error('unexpected intrinsic type')
|
|
}
|
|
} else if (param.type?.dereferenced) {
|
|
const dereferenced = param.type.dereferenced
|
|
|
|
if (dereferenced.children) {
|
|
children = dereferenced.children
|
|
} else if (dereferenced.type?.declaration?.children) {
|
|
children = dereferenced.type.declaration.children
|
|
} else if (dereferenced.type?.type === 'query') {
|
|
// skip: ignore types created from `typeof` for now, like `type Fetch = typeof fetch`
|
|
} else if (dereferenced.type?.type === 'union') {
|
|
// skip: we don't want to show unions as nested parameters
|
|
} else if (Object.keys(dereferenced).length === 0) {
|
|
// skip: {} have no children
|
|
} else {
|
|
throw new Error('unexpected case for dereferenced param type')
|
|
}
|
|
} else if (param.type?.type === 'reflection') {
|
|
const declaration = param.type.declaration
|
|
|
|
if (!declaration) {
|
|
throw new Error('reflection must have a declaration')
|
|
}
|
|
|
|
if (declaration.children) {
|
|
children = declaration.children
|
|
} else if (declaration.signatures) {
|
|
// skip: functions have no children
|
|
} else if (declaration.name === '__type') {
|
|
// skip: mostly inlined object type
|
|
} else {
|
|
throw new Error('unexpected case for reflection param type')
|
|
}
|
|
} else if (param.type?.type === 'indexedAccess') {
|
|
// skip: too complex, e.g. PromisifyMethods<Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>>
|
|
} else if (param.type?.type === 'reference') {
|
|
// skip: mostly unexported types
|
|
} else if (param.type?.type === 'union') {
|
|
// skip: we don't want to show unions as nested parameters
|
|
} else if (param.type?.type === 'array') {
|
|
// skip: no use for it for now
|
|
} else {
|
|
// skip: no use for now
|
|
//throw new Error(`unexpected param type`)
|
|
}
|
|
|
|
if (children) {
|
|
const properties = children
|
|
.sort((a, b) => a.name?.localeCompare(b.name)) // first alphabetical
|
|
.sort((a, b) => (a.flags?.isOptional ? 1 : -1)) // required params first
|
|
.map((x) => recurseThroughParams(x))
|
|
labelParams.subContent = properties
|
|
}
|
|
return labelParams
|
|
}
|
|
|
|
// const isDereferenced = (paramDefinition: TsDoc.TypeDefinition) => {
|
|
// // @ts-ignore
|
|
// return paramDefinition.type?.type == 'reference' && paramDefinition.type?.dereferenced?.id
|
|
// }
|
|
|
|
function generateLabelParam(param: any) {
|
|
let labelParams: any = {}
|
|
if (param.type?.type === 'intrinsic' && param.type?.name === 'unknown') {
|
|
labelParams = {
|
|
name: param.name ?? param.value,
|
|
isOptional: Boolean(param.flags?.isOptional) || 'defaultValue' in param,
|
|
type: 'any',
|
|
description: param.comment ? tsDocCommentToMdComment(param.comment) : null,
|
|
}
|
|
} else if (param.type?.declaration?.signatures) {
|
|
labelParams = {
|
|
name: param.name ?? param.value,
|
|
isOptional: Boolean(param.flags?.isOptional) || 'defaultValue' in param,
|
|
type: 'function',
|
|
description: param.comment ? tsDocCommentToMdComment(param.comment) : null,
|
|
}
|
|
} else if (param.type?.type === 'literal') {
|
|
labelParams = {
|
|
name: param.name ?? param.value,
|
|
isOptional: Boolean(param.flags?.isOptional) || 'defaultValue' in param,
|
|
type: typeof param.type.value === 'string' ? `"${param.type.value}"` : `${param.type.value}`,
|
|
description: param.comment ? tsDocCommentToMdComment(param.comment) : null,
|
|
}
|
|
} else {
|
|
labelParams = {
|
|
name: param.name ?? extractParamTypeAsString(param),
|
|
isOptional: Boolean(param.flags?.isOptional) || 'defaultValue' in param,
|
|
type: extractParamTypeAsString(param),
|
|
description: param.comment ? tsDocCommentToMdComment(param.comment) : null,
|
|
}
|
|
}
|
|
return labelParams
|
|
}
|
|
|
|
function extractParamTypeAsString(paramDefinition) {
|
|
if (paramDefinition.type?.name) {
|
|
// return `<code>${paramDefinition.type.name}</code>` // old
|
|
return paramDefinition.type.name
|
|
} else if (paramDefinition.type?.type === 'union') {
|
|
// only do this for literal/primitive types - for complex objects we just return 'object'
|
|
if (paramDefinition.type.types.every(({ type }) => ['literal', 'intrinsic'].includes(type))) {
|
|
return paramDefinition.type.types
|
|
.map((x) => {
|
|
if (x.type === 'literal') {
|
|
if (typeof x.value === 'string') {
|
|
return `"${x.value}"`
|
|
}
|
|
return `${x.value}`
|
|
} else if (x.type === 'intrinsic') {
|
|
if (x.name === 'unknown') {
|
|
return 'any'
|
|
}
|
|
return x.name
|
|
}
|
|
})
|
|
.join(' | ')
|
|
}
|
|
} else if (paramDefinition.type?.type === 'array') {
|
|
const elementType = paramDefinition.type.elementType
|
|
|
|
if (elementType.type === 'intrinsic') {
|
|
if (elementType.name === 'unknown') {
|
|
return 'any[]'
|
|
}
|
|
return `${elementType.name}[]`
|
|
}
|
|
|
|
return 'object[]'
|
|
}
|
|
|
|
return 'object' // old '<code>object</code>'
|
|
}
|
|
|
|
const tsDocCommentToMdComment = (commentObject: TsDoc.DocComment) =>
|
|
`
|
|
${commentObject?.shortText || ''}
|
|
|
|
${commentObject?.text || ''}
|
|
|
|
`.trim()
|
|
|
|
// function generateExamples(id: string, specExamples: any, allLanguages: any) {
|
|
// return specExamples.map((example) => {
|
|
// let allTabs = example.hideCodeBlock ? '' : generateCodeBlocks(allLanguages, example)
|
|
// return Example({
|
|
// name: example.name,
|
|
// description: example.description,
|
|
// tabs: allTabs,
|
|
// note: example.note,
|
|
// })
|
|
// })
|
|
// }
|
|
|
|
// OPENAPI-SPEC-VERSION: 3.0.0
|
|
type v3OperationWithPath = OpenAPIV3.OperationObject & {
|
|
path: string
|
|
}
|
|
|
|
export type enrichedOperation = OpenAPIV3.OperationObject & {
|
|
path: string
|
|
fullPath: string
|
|
operationId: string
|
|
operation: string
|
|
responseList: []
|
|
description?: string
|
|
parameters?: []
|
|
responses?: {}
|
|
security?: []
|
|
summary?: string
|
|
tags?: []
|
|
}
|
|
|
|
export function gen_v3(
|
|
spec: OpenAPIV3.Document,
|
|
dest: string,
|
|
{ apiUrl, type }: { apiUrl: string; type?: 'client-lib' | 'cli' | 'api' | 'mgmt-api' }
|
|
) {
|
|
const specLayout = spec.tags || []
|
|
const operations: enrichedOperation[] = []
|
|
|
|
Object.entries(spec.paths).forEach(([key, val]) => {
|
|
const fullPath = `${apiUrl}${key}`
|
|
|
|
toArrayWithKey(val!, 'operation').forEach((o) => {
|
|
const operation = o as v3OperationWithPath
|
|
const operationId =
|
|
type === 'mgmt-api' && operation.operationId && isValidSlug(operation.operationId)
|
|
? operation.operationId
|
|
: slugify(operation.summary!)
|
|
const enriched = {
|
|
...operation,
|
|
path: key,
|
|
fullPath,
|
|
operationId,
|
|
responseList: toArrayWithKey(operation.responses!, 'responseCode') || [],
|
|
}
|
|
// @ts-expect-error // missing 'responses', see OpenAPIV3.OperationObject.responses
|
|
operations.push(enriched)
|
|
})
|
|
})
|
|
|
|
const sections = specLayout.map((section) => {
|
|
return {
|
|
...section,
|
|
title: toTitle(section.name),
|
|
id: slugify(section.name),
|
|
operations: operations.filter((operation) => operation.tags?.includes(section.name)),
|
|
}
|
|
})
|
|
|
|
const content = {
|
|
info: spec.info,
|
|
sections,
|
|
operations,
|
|
}
|
|
|
|
return content
|
|
}
|
|
|
|
const slugify = (text: string) => {
|
|
if (!text) return ''
|
|
return text
|
|
.toString()
|
|
.toLowerCase()
|
|
.replace(/[. )(]/g, '-') // Replace spaces and brackets -
|
|
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
|
.replace(/\-\-+/g, '-') // Replace multiple - with single -
|
|
.replace(/^-+/, '') // Trim - from start of text
|
|
.replace(/-+$/, '') // Trim - from end of text
|
|
}
|
|
|
|
function isValidSlug(slug: string): boolean {
|
|
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
return slugRegex.test(slug)
|
|
}
|
|
|
|
// Uppercase the first letter of a string
|
|
const toTitle = (text: string) => {
|
|
return text.charAt(0).toUpperCase() + text.slice(1)
|
|
}
|
|
|
|
/**
|
|
* Convert Object to Array of values
|
|
*/
|
|
export const toArrayWithKey = (obj: object, keyAs: string) =>
|
|
values(
|
|
mapValues(obj, (value: any, key: string) => {
|
|
value[keyAs] = key
|
|
return value
|
|
})
|
|
)
|
|
|
|
/**
|
|
* Get a list of common section IDs that are available in this spec
|
|
*/
|
|
export function getAvailableSectionIds(sections: ICommonItem[], spec: any) {
|
|
// Filter parent sections first
|
|
|
|
const specIds = spec.functions.map(({ id }) => id)
|
|
|
|
const newShape = flattenSections(sections).filter((section) => {
|
|
if (specIds.includes(section.id)) {
|
|
return section
|
|
}
|
|
})
|
|
|
|
const final = newShape.map((func) => {
|
|
return func.id
|
|
})
|
|
|
|
return final
|
|
}
|