mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
added readOneBy, updateMany, deleteMany, exists
This commit is contained in:
@@ -40,19 +40,17 @@ describe("DataApi", () => {
|
||||
|
||||
{
|
||||
const res = (await app.request("/entity/posts")) as Response;
|
||||
const { data } = await res.json();
|
||||
const { data } = (await res.json()) as any;
|
||||
expect(data.length).toEqual(3);
|
||||
}
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 });
|
||||
// @ts-ignore protected
|
||||
api.fetcher = app.request as typeof fetch;
|
||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
|
||||
{
|
||||
const req = api.readMany("posts", { select: ["title"] });
|
||||
expect(req.request.method).toBe("GET");
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload);
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -64,7 +62,151 @@ describe("DataApi", () => {
|
||||
});
|
||||
expect(req.request.method).toBe("POST");
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload);
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
});
|
||||
|
||||
it("updates many", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [
|
||||
{ title: "foo", count: 0 },
|
||||
{ title: "bar", count: 0 },
|
||||
{ title: "baz", count: 0 },
|
||||
{ title: "bla", count: 2 },
|
||||
];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
{
|
||||
const req = api.readMany("posts", {
|
||||
select: ["title", "count"],
|
||||
});
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
// update with empty where
|
||||
expect(() => api.updateMany("posts", {}, { count: 1 })).toThrow();
|
||||
expect(() => api.updateMany("posts", undefined, { count: 1 })).toThrow();
|
||||
}
|
||||
|
||||
{
|
||||
// update
|
||||
const req = await api.updateMany("posts", { count: 0 }, { count: 1 });
|
||||
expect(req.res.status).toBe(200);
|
||||
}
|
||||
|
||||
{
|
||||
// compare
|
||||
const res = await api.readMany("posts", {
|
||||
select: ["title", "count"],
|
||||
});
|
||||
expect(res.map((p) => p.count)).toEqual([1, 1, 1, 2]);
|
||||
}
|
||||
});
|
||||
|
||||
it("refines", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
const normalOne = api.readOne("posts", 1);
|
||||
const normal = api.readMany("posts", { select: ["title"], where: { title: "baz" } });
|
||||
expect((await normal).data).toEqual([{ title: "baz" }] as any);
|
||||
|
||||
// refine
|
||||
const refined = normal.refine((data) => data[0]);
|
||||
expect((await refined).data).toEqual({ title: "baz" } as any);
|
||||
|
||||
// one
|
||||
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
|
||||
const oneByRes = await oneBy;
|
||||
expect(oneByRes.data).toEqual({ title: "baz" } as any);
|
||||
expect(oneByRes.body.meta.count).toEqual(1);
|
||||
});
|
||||
|
||||
it("exists/count", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
|
||||
await em.mutator("posts").insertMany(payload);
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
|
||||
const exists = api.exists("posts", { id: 1 });
|
||||
expect((await exists).exists).toBeTrue();
|
||||
|
||||
expect((await api.count("posts")).count).toEqual(3);
|
||||
});
|
||||
|
||||
it("creates many", async () => {
|
||||
const schema = proto.em({
|
||||
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
|
||||
});
|
||||
const em = schemaToEm(schema);
|
||||
await em.schema().sync({ force: true });
|
||||
|
||||
const payload = [
|
||||
{ title: "foo", count: 0 },
|
||||
{ title: "bar", count: 0 },
|
||||
{ title: "baz", count: 0 },
|
||||
{ title: "bla", count: 2 },
|
||||
];
|
||||
|
||||
const ctx: any = { em, guard: new Guard() };
|
||||
const controller = new DataController(ctx, dataConfig);
|
||||
const app = controller.getController();
|
||||
|
||||
// @ts-ignore tests
|
||||
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
|
||||
|
||||
{
|
||||
// create many
|
||||
const res = await api.createMany("posts", payload);
|
||||
expect(res.data.length).toEqual(4);
|
||||
expect(res.ok).toBeTrue();
|
||||
}
|
||||
|
||||
{
|
||||
const req = api.readMany("posts", {
|
||||
select: ["title", "count"],
|
||||
});
|
||||
const res = await req;
|
||||
expect(res.data).toEqual(payload as any);
|
||||
}
|
||||
|
||||
{
|
||||
// create with empty
|
||||
expect(() => api.createMany("posts", [])).toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,6 +89,14 @@ describe("ModuleApi", () => {
|
||||
expect(api.delete("/").request.method).toEqual("DELETE");
|
||||
});
|
||||
|
||||
it("refines", async () => {
|
||||
const app = new Hono().get("/endpoint", (c) => c.json({ foo: ["bar"] }));
|
||||
const api = new Api({ host }, app.request as typeof fetch);
|
||||
|
||||
expect((await api.get("/endpoint")).data).toEqual({ foo: ["bar"] });
|
||||
expect((await api.get("/endpoint").refine((data) => data.foo)).data).toEqual(["bar"]);
|
||||
});
|
||||
|
||||
// @todo: test error response
|
||||
// @todo: test method shortcut functions
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ describe("Mutator simple", async () => {
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
||||
//console.log((await em.repository(items).findMany()).data);
|
||||
|
||||
await em.mutator(items).deleteWhere();
|
||||
await em.mutator(items).deleteWhere({ id: { $isnull: 0 } });
|
||||
expect((await em.repository(items).findMany()).data.length).toBe(0);
|
||||
|
||||
//expect(res.data.count).toBe(0);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.9.0-rc.2",
|
||||
"version": "0.9.0-rc.3",
|
||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||
"homepage": "https://bknd.io",
|
||||
"repository": {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,14 +88,32 @@ const { data } = await api.data.readMany("posts", {
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
select: ["id", "title", "views"],
|
||||
with: ["comments"],
|
||||
with: {
|
||||
// join last 2 comments
|
||||
comments: {
|
||||
with: {
|
||||
// along with the comments' user
|
||||
users: {}
|
||||
},
|
||||
limit: 2,
|
||||
sort: "-id"
|
||||
},
|
||||
// also get the first 2 images, but only the path
|
||||
images: {
|
||||
select: ["path"],
|
||||
limit: 2
|
||||
}
|
||||
},
|
||||
where: {
|
||||
// same as '{ title: { $eg: "Hello, World!" } }'
|
||||
title: "Hello, World!",
|
||||
// only with views greater than 100
|
||||
views: {
|
||||
$gt: 100
|
||||
}
|
||||
},
|
||||
sort: { by: "views", order: "desc" }
|
||||
// sort by views descending (without "-" would be ascending)
|
||||
sort: "-views"
|
||||
});
|
||||
```
|
||||
The `with` property automatically adds the related entries to the response.
|
||||
@@ -116,6 +134,15 @@ const { data } = await api.data.createOne("posts", {
|
||||
});
|
||||
```
|
||||
|
||||
### `data.createMany([entity], [data])`
|
||||
To create many records of an entity, use the `createMany` method:
|
||||
```ts
|
||||
const { data } = await api.data.createMany("posts", [
|
||||
{ title: "Hello, World!" },
|
||||
{ title: "Again, Hello." },
|
||||
]);
|
||||
```
|
||||
|
||||
### `data.updateOne([entity], [id], [data])`
|
||||
To update a single record of an entity, use the `updateOne` method:
|
||||
```ts
|
||||
@@ -124,12 +151,26 @@ const { data } = await api.data.updateOne("posts", 1, {
|
||||
});
|
||||
```
|
||||
|
||||
### `data.updateMany([entity], [where], [update])`
|
||||
To update many records of an entity, use the `updateMany` method:
|
||||
```ts
|
||||
const { data } = await api.data.updateMany("posts", { views: { $gt: 1 } }, {
|
||||
title: "viewed more than once"
|
||||
});
|
||||
```
|
||||
|
||||
### `data.deleteOne([entity], [id])`
|
||||
To delete a single record of an entity, use the `deleteOne` method:
|
||||
```ts
|
||||
const { data } = await api.data.deleteOne("posts", 1);
|
||||
```
|
||||
|
||||
### `data.deleteMany([entity], [where])`
|
||||
To delete many records of an entity, use the `deleteMany` method:
|
||||
```ts
|
||||
const { data } = await api.data.deleteMany("posts", { views: { $lte: 1 } });
|
||||
```
|
||||
|
||||
## Auth (`api.auth`)
|
||||
Access the `Auth` specific API methods at `api.auth`. If there is successful authentication, the
|
||||
API will automatically save the token and use it for subsequent requests.
|
||||
|
||||
Reference in New Issue
Block a user