docs-utils: add operator map to docs generator knowledge base (#14382)

This commit is contained in:
Shahed Nasser
2025-12-22 14:41:01 +02:00
committed by GitHub
parent cb33388202
commit b221e882d4
2 changed files with 229 additions and 35 deletions

View File

@@ -1,14 +1,21 @@
import ts from "typescript"
import { OpenApiSchema } from "../../types/index.js" import { OpenApiSchema } from "../../types/index.js"
type SchemaMap = Record<
string,
OpenApiSchema | ((type: ts.Type) => OpenApiSchema)
>
/** /**
* This class has predefined OAS schemas for some types. It's used to bypass * This class has predefined OAS schemas for some types. It's used to bypass
* the logic of creating a schema for certain types. * the logic of creating a schema for certain types.
*/ */
class SchemaFactory { class SchemaFactory {
private checker: ts.TypeChecker
/** /**
* The pre-defined schemas. * The pre-defined schemas.
*/ */
private schemas: Record<string, OpenApiSchema> = { private schemas: SchemaMap = {
$and: { $and: {
type: "array", type: "array",
description: description:
@@ -68,7 +75,7 @@ class SchemaFactory {
/** /**
* Schemas used only for query types * Schemas used only for query types
*/ */
private schemasForQuery: Record<string, OpenApiSchema> = { private schemasForQuery: SchemaMap = {
expand: { expand: {
type: "string", type: "string",
title: "expand", title: "expand",
@@ -118,12 +125,167 @@ class SchemaFactory {
description: "Learn how to manage metadata", description: "Learn how to manage metadata",
}, },
}, },
OperatorMap: (type) => {
if (!("typeArguments" in type || "aliasTypeArguments" in type)) {
return {}
}
const typeRef = type as ts.TypeReference
const typeArgs = typeRef.typeArguments || typeRef.aliasTypeArguments
if (!typeArgs || typeArgs.length === 0) {
return {}
}
const typeStr = this.checker.typeToString(typeArgs[0])
return {
type: "object",
properties: {
$and: JSON.parse(JSON.stringify(this.schemas["$and"])),
$or: JSON.parse(JSON.stringify(this.schemas["$or"])),
$eq: {
oneOf: [
{
type: typeStr,
title: "$eq",
description: "Filter by exact value.",
},
{
type: "array",
title: "$eq",
description: "Filter by exact value.",
items: {
type: typeStr,
},
},
],
},
$ne: {
type: typeStr,
title: "$ne",
description: "Filter by not equal to the given value.",
},
$in: {
type: "array",
title: "$in",
description: "Filter by values included in the given array.",
items: {
type: typeStr,
},
},
$nin: {
type: "array",
title: "$nin",
description: "Filter by values not included in the given array.",
items: {
type: typeStr,
},
},
$not: {
oneOf: [
{
type: typeStr,
title: "$not",
description: "Filter by not equal to the given value.",
},
{
type: "object",
title: "$not",
description:
"Filter by values not matching the conditions in this parameter.",
},
{
type: "array",
title: "$not",
description:
"Filter by values not matching the conditions in this parameter.",
items: {
type: typeStr,
},
},
],
},
$gt: {
type: typeStr,
title: "$gt",
description: "Filter by values greater than the given value.",
},
$gte: {
type: typeStr,
title: "$gte",
description:
"Filter by values greater than or equal to the given value.",
},
$lt: {
type: typeStr,
title: "$lt",
description: "Filter by values less than the given value.",
},
$lte: {
type: typeStr,
title: "$lte",
description:
"Filter by values less than or equal to the given value.",
},
$like: {
type: typeStr,
title: "$like",
description: "Apply a `like` filter. Useful for strings only.",
},
$re: {
type: typeStr,
title: "$re",
description: "Apply a regex filter. Useful for strings only.",
},
$ilike: {
type: typeStr,
title: "$ilike",
description:
"Apply a case-insensitive `like` filter. Useful for strings only.",
},
$fulltext: {
type: typeStr,
title: "$fulltext",
description: "Filter to apply on full-text properties.",
},
$overlap: {
type: "array",
title: "$overlap",
description:
"Filter to apply on array properties to find overlapping values.",
items: {
type: typeStr,
},
},
$contains: {
type: "array",
title: "$contains",
description:
"Filter to apply on array properties to find contained values.",
items: {
type: typeStr,
},
},
$contained: {
type: "array",
title: "$contained",
description:
"Filter to apply on array properties to find contained values.",
items: {
type: typeStr,
},
},
$exists: {
type: "boolean",
title: "$exists",
description: "Filter by whether a value exists or not.",
},
},
} as OpenApiSchema
},
} }
/** /**
* Schemas used only for response types. * Schemas used only for response types.
*/ */
private schemasForResponse: Record<string, OpenApiSchema> = { private schemasForResponse: SchemaMap = {
created_at: { created_at: {
type: "string", type: "string",
format: "date-time", format: "date-time",
@@ -138,6 +300,10 @@ class SchemaFactory {
}, },
} }
constructor({ checker }: { checker: ts.TypeChecker }) {
this.checker = checker
}
/** /**
* Try to retrieve the pre-defined schema of a type name. * Try to retrieve the pre-defined schema of a type name.
* *
@@ -145,17 +311,23 @@ class SchemaFactory {
* @param additionalData - Additional data to pass along/override in the predefined schema. For example, a description. * @param additionalData - Additional data to pass along/override in the predefined schema. For example, a description.
* @returns The schema, if found. * @returns The schema, if found.
*/ */
public tryGetSchema( public tryGetSchema({
name: string, name,
additionalData?: Partial<OpenApiSchema>, additionalData,
type: "request" | "query" | "response" | "all" = "all" context = "all",
): OpenApiSchema | undefined { type,
}: {
name: string
additionalData?: Partial<OpenApiSchema>
context?: "request" | "query" | "response" | "all"
type?: ts.Type
}): OpenApiSchema | undefined {
const schemasFactory = const schemasFactory =
type === "response" context === "response"
? this.mergeSchemas(this.schemasForResponse, this.schemas) ? this.mergeSchemas(this.schemasForResponse, this.schemas)
: type === "query" : context === "query"
? this.mergeSchemas(this.schemasForQuery, this.schemas) ? this.mergeSchemas(this.schemasForQuery, this.schemas)
: this.cloneSchema(this.schemas) : this.mergeSchemas(this.schemas)
const key = Object.hasOwn(schemasFactory, name) const key = Object.hasOwn(schemasFactory, name)
? name ? name
: additionalData?.title || "" : additionalData?.title || ""
@@ -163,7 +335,17 @@ class SchemaFactory {
return return
} }
let schema = Object.assign({}, schemasFactory[key]) let schema: OpenApiSchema | undefined
if (typeof schemasFactory[key] === "function") {
if (!type) {
return
}
schema = schemasFactory[key](type)
} else {
schema = Object.assign({}, schemasFactory[key])
}
if (additionalData) { if (additionalData) {
schema = Object.assign(schema, { schema = Object.assign(schema, {
@@ -176,17 +358,16 @@ class SchemaFactory {
return schema return schema
} }
private mergeSchemas( private mergeSchemas(...schemas: SchemaMap[]): SchemaMap {
main: Record<string, OpenApiSchema>, return schemas.reduce((merged, schema) => {
other: Record<string, OpenApiSchema> Object.entries(schema).forEach(([key, value]) => {
): Record<string, OpenApiSchema> { merged[key] =
return Object.assign(this.cloneSchema(main), this.cloneSchema(other)) typeof value === "function"
} ? value
: JSON.parse(JSON.stringify(value))
private cloneSchema( })
schema: Record<string, OpenApiSchema> return merged
): Record<string, OpenApiSchema> { }, {} as SchemaMap)
return JSON.parse(JSON.stringify(schema))
} }
} }

View File

@@ -160,7 +160,7 @@ class OasKindGenerator extends FunctionKindGenerator {
this.tags = new Map() this.tags = new Map()
this.oasSchemaHelper = new OasSchemaHelper() this.oasSchemaHelper = new OasSchemaHelper()
this.schemaFactory = new SchemaFactory() this.schemaFactory = new SchemaFactory({ checker: this.checker })
this.typesHelper = new TypesHelper({ this.typesHelper = new TypesHelper({
checker: this.checker, checker: this.checker,
}) })
@@ -1549,17 +1549,30 @@ class OasKindGenerator extends FunctionKindGenerator {
const typeAsString = const typeAsString =
zodObjectTypeName || this.checker.typeToString(itemType) zodObjectTypeName || this.checker.typeToString(itemType)
const schemaFromFactory = this.schemaFactory.tryGetSchema( const schemaFromFactory =
itemType.symbol?.getName() || this.schemaFactory.tryGetSchema({
itemType.aliasSymbol?.getName() || name:
title || itemType.symbol?.getName() ||
typeAsString, itemType.aliasSymbol?.getName() ||
{ title ||
title: title || typeAsString, typeAsString,
description, additionalData: {
}, title: title || typeAsString,
rest.context description,
) },
context: rest.context,
type: itemType,
}) ||
this.schemaFactory.tryGetSchema({
// remove type arguments from name
name: typeAsString.replace(/<.*>$/, ""),
additionalData: {
title: title || typeAsString,
description,
},
context: rest.context,
type: itemType,
})
if (schemaFromFactory) { if (schemaFromFactory) {
return schemaFromFactory return schemaFromFactory