diff --git a/www/utils/packages/docs-generator/src/classes/helpers/schema-factory.ts b/www/utils/packages/docs-generator/src/classes/helpers/schema-factory.ts index cdca41865c..910c207e20 100644 --- a/www/utils/packages/docs-generator/src/classes/helpers/schema-factory.ts +++ b/www/utils/packages/docs-generator/src/classes/helpers/schema-factory.ts @@ -1,14 +1,21 @@ +import ts from "typescript" 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 * the logic of creating a schema for certain types. */ class SchemaFactory { + private checker: ts.TypeChecker /** * The pre-defined schemas. */ - private schemas: Record = { + private schemas: SchemaMap = { $and: { type: "array", description: @@ -68,7 +75,7 @@ class SchemaFactory { /** * Schemas used only for query types */ - private schemasForQuery: Record = { + private schemasForQuery: SchemaMap = { expand: { type: "string", title: "expand", @@ -118,12 +125,167 @@ class SchemaFactory { 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. */ - private schemasForResponse: Record = { + private schemasForResponse: SchemaMap = { created_at: { type: "string", 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. * @@ -145,17 +311,23 @@ class SchemaFactory { * @param additionalData - Additional data to pass along/override in the predefined schema. For example, a description. * @returns The schema, if found. */ - public tryGetSchema( - name: string, - additionalData?: Partial, - type: "request" | "query" | "response" | "all" = "all" - ): OpenApiSchema | undefined { + public tryGetSchema({ + name, + additionalData, + context = "all", + type, + }: { + name: string + additionalData?: Partial + context?: "request" | "query" | "response" | "all" + type?: ts.Type + }): OpenApiSchema | undefined { const schemasFactory = - type === "response" + context === "response" ? this.mergeSchemas(this.schemasForResponse, this.schemas) - : type === "query" + : context === "query" ? this.mergeSchemas(this.schemasForQuery, this.schemas) - : this.cloneSchema(this.schemas) + : this.mergeSchemas(this.schemas) const key = Object.hasOwn(schemasFactory, name) ? name : additionalData?.title || "" @@ -163,7 +335,17 @@ class SchemaFactory { 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) { schema = Object.assign(schema, { @@ -176,17 +358,16 @@ class SchemaFactory { return schema } - private mergeSchemas( - main: Record, - other: Record - ): Record { - return Object.assign(this.cloneSchema(main), this.cloneSchema(other)) - } - - private cloneSchema( - schema: Record - ): Record { - return JSON.parse(JSON.stringify(schema)) + private mergeSchemas(...schemas: SchemaMap[]): SchemaMap { + return schemas.reduce((merged, schema) => { + Object.entries(schema).forEach(([key, value]) => { + merged[key] = + typeof value === "function" + ? value + : JSON.parse(JSON.stringify(value)) + }) + return merged + }, {} as SchemaMap) } } diff --git a/www/utils/packages/docs-generator/src/classes/kinds/oas.ts b/www/utils/packages/docs-generator/src/classes/kinds/oas.ts index be83772985..76dd5e910a 100644 --- a/www/utils/packages/docs-generator/src/classes/kinds/oas.ts +++ b/www/utils/packages/docs-generator/src/classes/kinds/oas.ts @@ -160,7 +160,7 @@ class OasKindGenerator extends FunctionKindGenerator { this.tags = new Map() this.oasSchemaHelper = new OasSchemaHelper() - this.schemaFactory = new SchemaFactory() + this.schemaFactory = new SchemaFactory({ checker: this.checker }) this.typesHelper = new TypesHelper({ checker: this.checker, }) @@ -1549,17 +1549,30 @@ class OasKindGenerator extends FunctionKindGenerator { const typeAsString = zodObjectTypeName || this.checker.typeToString(itemType) - const schemaFromFactory = this.schemaFactory.tryGetSchema( - itemType.symbol?.getName() || - itemType.aliasSymbol?.getName() || - title || - typeAsString, - { - title: title || typeAsString, - description, - }, - rest.context - ) + const schemaFromFactory = + this.schemaFactory.tryGetSchema({ + name: + itemType.symbol?.getName() || + itemType.aliasSymbol?.getName() || + title || + typeAsString, + additionalData: { + title: title || typeAsString, + 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) { return schemaFromFactory