chanfana
Version:
OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!
184 lines (160 loc) • 5.42 kB
text/typescript
import { z } from "zod";
import { contentJson } from "../contentTypes";
import { InputValidationException } from "../exceptions";
import { OpenAPIRoute } from "../route";
import type { AnyZodObject, OrderByDirection } from "../types";
import {
type FilterCondition,
type ListFilters,
type ListResult,
MetaGenerator,
type MetaInput,
metaSchemaProps,
type O,
} from "./types";
export class ListEndpoint<HandleArgs extends Array<object> = Array<object>> extends OpenAPIRoute<HandleArgs> {
// @ts-expect-error
_meta: MetaInput;
get meta() {
return MetaGenerator(this._meta);
}
filterFields?: Array<string>;
searchFields?: Array<string>;
searchFieldName = "search";
pageFieldName = "page";
perPageFieldName = "per_page";
orderByFieldName = "order_by";
orderByDirectionFieldName = "order_by_direction";
// Explicitly type orderByFields to avoid narrow never[] inference for subclasses
orderByFields: string[] = [];
defaultOrderBy?: string;
/** Default sort direction when order_by is used. Defaults to "asc". */
defaultOrderByDirection: OrderByDirection = "asc";
get optionFields(): string[] {
return [this.pageFieldName, this.perPageFieldName, this.orderByFieldName, this.orderByDirectionFieldName];
}
getSchema() {
const parsedQueryParameters = this.meta.fields.pick(
(this.filterFields || [])
.filter((item) => !new Set(this.params.urlParams || []).has(item))
.reduce((a, v) => ({ ...a, [v]: true }), {}),
).shape;
const pathParameters = this.meta.fields.pick(
(this.params.urlParams || this.meta.model.primaryKeys || []).reduce((a, v) => ({ ...a, [v]: true }), {}),
);
for (const [key, value] of Object.entries(parsedQueryParameters)) {
// @ts-expect-error TODO: check this
parsedQueryParameters[key] = (value as AnyZodObject).optional();
}
if (this.searchFields) {
// @ts-expect-error TODO: check this
parsedQueryParameters[this.searchFieldName] = z
.string()
.optional()
.openapi({
description: `Search by ${this.searchFields.join(", ")}`,
});
}
let queryParameters = z
.object({
[this.pageFieldName]: z.number().int().min(1).optional().default(1),
[this.perPageFieldName]: z.number().int().min(1).max(100).optional().default(20),
})
.extend(parsedQueryParameters);
if (this.orderByFields && this.orderByFields.length > 0) {
const orderByFieldsTuple = this.orderByFields as [string, ...string[]];
queryParameters = queryParameters.extend({
[this.orderByFieldName]: z
.enum(orderByFieldsTuple)
.optional()
.default(orderByFieldsTuple[0])
.describe("Order By Column Name"),
[this.orderByDirectionFieldName]: z
.enum(["asc", "desc"])
.optional()
.default(this.defaultOrderByDirection)
.describe("Order By Direction"),
} as any); // Cast needed: computed property keys widen the type beyond what Zod's .extend() accepts
}
return {
request: {
params: Object.keys(pathParameters.shape).length ? pathParameters : undefined,
query: queryParameters,
...this.schema?.request,
},
responses: {
"200": {
description: "List objects",
...contentJson(
z.object({
success: z.boolean(),
result: z.array(this.meta.model.serializerSchema),
}),
),
...this.schema?.responses?.[200],
},
...InputValidationException.schema(),
...this.schema?.responses,
},
...metaSchemaProps(this._meta),
...this.schema,
};
}
async getFilters(): Promise<ListFilters> {
const data = await this.getValidatedData();
const filters: Array<FilterCondition> = [];
const options: Record<string, string> = {}; // TODO: fix this type
for (const part of [data.params, data.query]) {
if (part) {
for (const [key, value] of Object.entries(part)) {
if (this.searchFields && key === this.searchFieldName) {
filters.push({
field: key,
operator: "LIKE",
value: value as string,
});
} else if (this.optionFields.includes(key)) {
options[key] = value as string;
} else {
filters.push({
field: key,
operator: "EQ",
value: value as string,
});
}
}
}
}
return {
options,
filters,
};
}
async before(filters: ListFilters): Promise<ListFilters> {
return filters;
}
async after(data: ListResult<O<typeof this._meta>>): Promise<ListResult<O<typeof this._meta>>> {
return data;
}
async list(_filters: ListFilters): Promise<ListResult<O<typeof this._meta>>> {
return {
result: [],
};
}
async handle(..._args: HandleArgs) {
let filters = await this.getFilters();
filters = await this.before(filters);
let objs = await this.list(filters);
objs = await this.after(objs);
objs = {
...objs,
result: objs.result.map((item) =>
this.meta.model.serializer(item, { filters: filters.filters, options: filters.options }),
),
};
return {
success: true,
...objs,
};
}
}