added readOneBy, updateMany, deleteMany, exists

This commit is contained in:
dswbx
2025-03-05 08:02:57 +01:00
parent 4f52537ea0
commit ef629321ab
9 changed files with 299 additions and 16 deletions

View File

@@ -1,6 +1,7 @@
import type { DB } from "core";
import type { EntityData, RepoQueryIn, RepositoryResponse } from "data";
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
export type DataApiOptions = BaseModuleApiOptions & {
queryLengthLimit: number;
@@ -18,6 +19,12 @@ export class DataApi extends ModuleApi<DataApiOptions> {
};
}
private requireObjectSet(obj: any, message?: string) {
if (!obj || typeof obj !== "object" || Object.keys(obj).length === 0) {
throw new Error(message ?? "object is required");
}
}
readOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
id: PrimaryFieldType,
@@ -29,6 +36,18 @@ export class DataApi extends ModuleApi<DataApiOptions> {
);
}
readOneBy<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
query: Omit<RepoQueryIn, "limit" | "offset" | "sort"> = {},
) {
type T = Pick<RepositoryResponse<Data>, "meta" | "data">;
return this.readMany(entity, {
...query,
limit: 1,
offset: 0,
}).refine((data) => data[0]) as unknown as FetchPromise<ResponseObject<T>>;
}
readMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
query: RepoQueryIn = {},
@@ -63,25 +82,64 @@ export class DataApi extends ModuleApi<DataApiOptions> {
return this.post<RepositoryResponse<Data>>(["entity", entity as any], input);
}
createMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
input: Omit<Data, "id">[],
) {
if (!input || !Array.isArray(input) || input.length === 0) {
throw new Error("input is required");
}
return this.post<RepositoryResponse<Data[]>>(["entity", entity as any], input);
}
updateOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
id: PrimaryFieldType,
input: Partial<Omit<Data, "id">>,
) {
if (!id) throw new Error("ID is required");
return this.patch<RepositoryResponse<Data>>(["entity", entity as any, id], input);
}
updateMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
where: RepoQueryIn["where"],
update: Partial<Omit<Data, "id">>,
) {
this.requireObjectSet(where);
return this.patch<RepositoryResponse<Data[]>>(["entity", entity as any], {
update,
where,
});
}
deleteOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
id: PrimaryFieldType,
) {
if (!id) throw new Error("ID is required");
return this.delete<RepositoryResponse<Data>>(["entity", entity as any, id]);
}
deleteMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
entity: E,
where: RepoQueryIn["where"],
) {
this.requireObjectSet(where);
return this.delete<RepositoryResponse<Data>>(["entity", entity as any], where);
}
count<E extends keyof DB | string>(entity: E, where: RepoQueryIn["where"] = {}) {
return this.post<RepositoryResponse<{ entity: E; count: number }>>(
["entity", entity as any, "fn", "count"],
where,
);
}
exists<E extends keyof DB | string>(entity: E, where: RepoQueryIn["where"] = {}) {
return this.post<RepositoryResponse<{ entity: E; exists: boolean }>>(
["entity", entity as any, "fn", "exists"],
where,
);
}
}

View File

@@ -343,14 +343,20 @@ export class DataController extends Controller {
"/:entity",
permission(DataPermissions.entityCreate),
tb("param", Type.Object({ entity: Type.String() })),
tb("json", Type.Union([Type.Object({}), Type.Array(Type.Object({}))])),
async (c) => {
const { entity } = c.req.param();
if (!this.entityExists(entity)) {
return this.notFound(c);
}
const body = (await c.req.json()) as EntityData;
const result = await this.em.mutator(entity).insertOne(body);
const body = (await c.req.json()) as EntityData | EntityData[];
if (Array.isArray(body)) {
const result = await this.em.mutator(entity).insertMany(body);
return c.json(this.mutatorResult(result), 201);
}
const result = await this.em.mutator(entity).insertOne(body);
return c.json(this.mutatorResult(result), 201);
},
);

View File

@@ -270,9 +270,14 @@ export class Mutator<
}
// @todo: decide whether entries should be deleted all at once or one by one (for events)
async deleteWhere(where?: RepoQuery["where"]): Promise<MutatorResponse<Output[]>> {
async deleteWhere(where: RepoQuery["where"]): Promise<MutatorResponse<Output[]>> {
const entity = this.entity;
// @todo: add a way to delete all by adding force?
if (!where || typeof where !== "object" || Object.keys(where).length === 0) {
throw new Error("Where clause must be provided for mass deletion");
}
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
entity.getSelect(),
);
@@ -282,11 +287,16 @@ export class Mutator<
async updateWhere(
data: Partial<Input>,
where?: RepoQuery["where"],
where: RepoQuery["where"],
): Promise<MutatorResponse<Output[]>> {
const entity = this.entity;
const validatedData = await this.getValidatedData(data, "update");
// @todo: add a way to delete all by adding force?
if (!where || typeof where !== "object" || Object.keys(where).length === 0) {
throw new Error("Where clause must be provided for mass update");
}
const query = this.appendWhere(this.conn.updateTable(entity.name), where)
.set(validatedData as any)
.returning(entity.getSelect());

View File

@@ -148,9 +148,10 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModul
});
}
delete<Data = any>(_input: TInput, _init?: RequestInit) {
delete<Data = any>(_input: TInput, body?: any, _init?: RequestInit) {
return this.request<Data>(_input, undefined, {
..._init,
body,
method: "DELETE",
});
}
@@ -171,7 +172,7 @@ export function createResponseProxy<Body = any, Data = any>(
body: Body,
data?: Data,
): ResponseObject<Body, Data> {
let actualData: any = data ?? body;
let actualData: any = typeof data !== "undefined" ? data : body;
const _props = ["raw", "body", "ok", "status", "res", "data", "toJSON"];
// that's okay, since you have to check res.ok anyway
@@ -189,6 +190,7 @@ export function createResponseProxy<Body = any, Data = any>(
if (prop === "toJSON") {
return () => target;
}
return Reflect.get(target, prop, receiver);
},
has(target, prop) {
@@ -223,12 +225,19 @@ export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
fetcher?: typeof fetch;
verbose?: boolean;
},
protected refineData?: (data: T) => any,
) {}
get verbose() {
return this.options?.verbose ?? false;
}
refine<N>(fn: (data: T) => N) {
return new FetchPromise(this.request, this.options, fn) as unknown as FetchPromise<
ApiResponse<N>
>;
}
async execute(): Promise<ResponseObject<T>> {
// delay in dev environment
isDebug() && (await new Promise((resolve) => setTimeout(resolve, 200)));
@@ -265,6 +274,15 @@ export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
resBody = res.body;
}
if (this.refineData) {
try {
resData = this.refineData(resData);
} catch (e) {
console.warn("[FetchPromise] Error in refineData", e);
resData = undefined;
}
}
return createResponseProxy<T>(res, resBody, resData);
}