docs-utils: add operator map to docs generator knowledge base (#14382)
This commit is contained in:
@@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user