UNPKG

chanfana

Version:

OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!

156 lines (129 loc) 4.37 kB
import { z } from "zod"; import { contentJson } from "../contentTypes"; import { InputValidationException, NotFoundException } from "../exceptions"; import { OpenAPIRoute } from "../route"; import { type FilterCondition, type Filters, MetaGenerator, type MetaInput, metaSchemaProps, type O, type UpdatedData, type UpdateFilters, } from "./types"; export class UpdateEndpoint<HandleArgs extends Array<object> = Array<object>> extends OpenAPIRoute<HandleArgs> { // @ts-expect-error _meta: MetaInput; get meta() { return MetaGenerator(this._meta); } getSchema() { const bodyParameters = this.meta.fields.omit( (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), ); const pathParameters = this.meta.model.schema.pick( (this.params.urlParams || []).reduce((a, v) => ({ ...a, [v]: true }), {}), ); return { request: { body: contentJson(bodyParameters), params: Object.keys(pathParameters.shape).length ? pathParameters : undefined, ...this.schema?.request, }, responses: { "200": { description: "Returns the updated Object", ...contentJson( z.object({ success: z.boolean(), result: this.meta.model.serializerSchema, }), ), ...this.schema?.responses?.[200], }, ...InputValidationException.schema(), ...NotFoundException.schema(), ...this.schema?.responses, }, ...metaSchemaProps(this._meta), ...this.schema, }; } async getFilters(): Promise<Filters> { const data = await this.getValidatedData(); const filters: Array<FilterCondition> = []; for (const part of [data.params, data.body]) { if (part) { for (const [key, value] of Object.entries(part)) { if ((this.meta.model.primaryKeys || []).includes(key)) { filters.push({ field: key, operator: "EQ", value: value as string, }); } } } } return { filters, }; } async getUpdatedData(_oldObj: O<typeof this._meta>): Promise<UpdatedData> { const data = await this.getValidatedData(); const updatedData = _oldObj; // In Zod 4, optional fields with defaults are always present in validated data, // even if not sent in the request. We need to check the raw unvalidated data to determine // which fields were actually provided vs which were added by Zod defaults. const rawData = await this.getUnvalidatedData(); for (const part of [data.params, data.body]) { if (part) { // Get corresponding raw part to check which fields were actually sent const rawPart = part === data.params ? rawData.params : rawData.body; for (const [key, value] of Object.entries(part)) { if (!(this.meta.model.primaryKeys || []).includes(key)) { // Only update if field was present in the raw request // This prevents Zod 4 defaults from overwriting existing values if (rawPart && key in rawPart) { updatedData[key] = value as string; } } } } } return { updatedData, }; } async before(_oldObj: O<typeof this._meta>, filters: UpdateFilters): Promise<UpdateFilters> { return filters; } async after<T = O<typeof this._meta>>(data: T): Promise<T> { return data; } async getObject(_filters: Filters): Promise<O<typeof this._meta> | null> { return null; } async update(oldObj: O<typeof this._meta>, _filters: UpdateFilters): Promise<O<typeof this._meta>> { return oldObj; } async handle(..._args: HandleArgs) { const initialFilters = await this.getFilters(); const oldObj = await this.getObject(initialFilters); if (oldObj === null) { throw new NotFoundException(); } const updatedData = await this.getUpdatedData(oldObj); const filters = await this.before(oldObj, { filters: initialFilters.filters, updatedData: updatedData.updatedData, }); let obj = await this.update(oldObj, filters); obj = await this.after(obj); return { success: true, result: this.meta.model.serializer(obj, { filters: filters.filters }), }; } }