chanfana
Version:
OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!
156 lines (129 loc) • 4.37 kB
text/typescript
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 }),
};
}
}