mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge pull request #104 from bknd-io/feat/additional-data-api-methods
feat/additional-data-api-methods
This commit is contained in:
@@ -40,19 +40,17 @@ describe("DataApi", () => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const res = (await app.request("/entity/posts")) as Response;
|
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);
|
expect(data.length).toEqual(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore tests
|
// @ts-ignore tests
|
||||||
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 });
|
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
|
||||||
// @ts-ignore protected
|
|
||||||
api.fetcher = app.request as typeof fetch;
|
|
||||||
{
|
{
|
||||||
const req = api.readMany("posts", { select: ["title"] });
|
const req = api.readMany("posts", { select: ["title"] });
|
||||||
expect(req.request.method).toBe("GET");
|
expect(req.request.method).toBe("GET");
|
||||||
const res = await req;
|
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");
|
expect(req.request.method).toBe("POST");
|
||||||
const res = await req;
|
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");
|
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 error response
|
||||||
// @todo: test method shortcut functions
|
// @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);
|
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
|
||||||
//console.log((await em.repository(items).findMany()).data);
|
//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((await em.repository(items).findMany()).data.length).toBe(0);
|
||||||
|
|
||||||
//expect(res.data.count).toBe(0);
|
//expect(res.data.count).toBe(0);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { _jsonp } from "../../../src/core/utils";
|
|
||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
@@ -10,7 +9,7 @@ import {
|
|||||||
WithBuilder,
|
WithBuilder,
|
||||||
} from "../../../src/data";
|
} from "../../../src/data";
|
||||||
import * as proto from "../../../src/data/prototype";
|
import * as proto from "../../../src/data/prototype";
|
||||||
import { compileQb, prettyPrintQb, schemaToEm } from "../../helper";
|
import { schemaToEm } from "../../helper";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -30,7 +29,7 @@ describe("[data] WithBuilder", async () => {
|
|||||||
);
|
);
|
||||||
const em = schemaToEm(schema);
|
const em = schemaToEm(schema);
|
||||||
|
|
||||||
expect(WithBuilder.validateWiths(em, "posts", undefined)).toBe(0);
|
expect(WithBuilder.validateWiths(em, "posts", undefined as any)).toBe(0);
|
||||||
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
|
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
|
||||||
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
|
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"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.",
|
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||||
"homepage": "https://bknd.io",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { SafeUser } from "auth";
|
|||||||
import { AuthApi } from "auth/api/AuthApi";
|
import { AuthApi } from "auth/api/AuthApi";
|
||||||
import { DataApi } from "data/api/DataApi";
|
import { DataApi } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
import { omit } from "lodash-es";
|
|
||||||
import { MediaApi } from "media/api/MediaApi";
|
import { MediaApi } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
|
import { omitKeys } from "core/utils";
|
||||||
|
|
||||||
export type TApiUser = SafeUser;
|
export type TApiUser = SafeUser;
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ export class Api {
|
|||||||
this.verified = false;
|
this.verified = false;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.user = omit(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
||||||
} else {
|
} else {
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export type Primitive = string | number | boolean;
|
import type { PrimaryFieldType } from "core";
|
||||||
|
|
||||||
|
export type Primitive = PrimaryFieldType | string | number | boolean;
|
||||||
export function isPrimitive(value: any): value is Primitive {
|
export function isPrimitive(value: any): value is Primitive {
|
||||||
return ["string", "number", "boolean"].includes(typeof value);
|
return ["string", "number", "boolean"].includes(typeof value);
|
||||||
}
|
}
|
||||||
@@ -63,7 +65,6 @@ function _convert<Exps extends Expressions>(
|
|||||||
expressions: Exps,
|
expressions: Exps,
|
||||||
path: string[] = [],
|
path: string[] = [],
|
||||||
): FilterQuery<Exps> {
|
): FilterQuery<Exps> {
|
||||||
//console.log("-----------------");
|
|
||||||
const ExpressionConditionKeys = expressions.map((e) => e.key);
|
const ExpressionConditionKeys = expressions.map((e) => e.key);
|
||||||
const keys = Object.keys($query);
|
const keys = Object.keys($query);
|
||||||
const operands = [OperandOr] as const;
|
const operands = [OperandOr] as const;
|
||||||
@@ -132,8 +133,6 @@ function _build<Exps extends Expressions>(
|
|||||||
): ValidationResults {
|
): ValidationResults {
|
||||||
const $query = options.convert ? _convert<Exps>(_query, expressions) : _query;
|
const $query = options.convert ? _convert<Exps>(_query, expressions) : _query;
|
||||||
|
|
||||||
//console.log("-----------------", { $query });
|
|
||||||
//const keys = Object.keys($query);
|
|
||||||
const result: ValidationResults = {
|
const result: ValidationResults = {
|
||||||
$and: [],
|
$and: [],
|
||||||
$or: [],
|
$or: [],
|
||||||
@@ -150,22 +149,16 @@ function _build<Exps extends Expressions>(
|
|||||||
if (!exp.valid(expected)) {
|
if (!exp.valid(expected)) {
|
||||||
throw new Error(`Invalid expected value at "${[...path, $op].join(".")}": ${expected}`);
|
throw new Error(`Invalid expected value at "${[...path, $op].join(".")}": ${expected}`);
|
||||||
}
|
}
|
||||||
//console.log("found exp", { key: exp.key, expected, actual });
|
|
||||||
return exp.validate(expected, actual, options.exp_ctx);
|
return exp.validate(expected, actual, options.exp_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check $and
|
// check $and
|
||||||
//console.log("$and entries", Object.entries($and));
|
|
||||||
for (const [key, value] of Object.entries($and)) {
|
for (const [key, value] of Object.entries($and)) {
|
||||||
//console.log("$op/$v", Object.entries(value));
|
|
||||||
for (const [$op, $v] of Object.entries(value)) {
|
for (const [$op, $v] of Object.entries(value)) {
|
||||||
const objValue = options.value_is_kv ? key : options.object[key];
|
const objValue = options.value_is_kv ? key : options.object[key];
|
||||||
//console.log("--check $and", { key, value, objValue, v_i_kv: options.value_is_kv });
|
|
||||||
//console.log("validate", { $op, $v, objValue, key });
|
|
||||||
result.$and.push(__validate($op, $v, objValue, [key]));
|
result.$and.push(__validate($op, $v, objValue, [key]));
|
||||||
result.keys.add(key);
|
result.keys.add(key);
|
||||||
}
|
}
|
||||||
//console.log("-", { key, value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check $or
|
// check $or
|
||||||
@@ -173,14 +166,11 @@ function _build<Exps extends Expressions>(
|
|||||||
const objValue = options.value_is_kv ? key : options.object[key];
|
const objValue = options.value_is_kv ? key : options.object[key];
|
||||||
|
|
||||||
for (const [$op, $v] of Object.entries(value)) {
|
for (const [$op, $v] of Object.entries(value)) {
|
||||||
//console.log("validate", { $op, $v, objValue });
|
|
||||||
result.$or.push(__validate($op, $v, objValue, [key]));
|
result.$or.push(__validate($op, $v, objValue, [key]));
|
||||||
result.keys.add(key);
|
result.keys.add(key);
|
||||||
}
|
}
|
||||||
//console.log("-", { key, value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("matches", matches);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { DB } from "core";
|
import type { DB } from "core";
|
||||||
import type { EntityData, RepoQueryIn, RepositoryResponse } from "data";
|
import type { EntityData, RepoQueryIn, RepositoryResponse } from "data";
|
||||||
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
|
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
|
||||||
|
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
|
||||||
|
|
||||||
export type DataApiOptions = BaseModuleApiOptions & {
|
export type DataApiOptions = BaseModuleApiOptions & {
|
||||||
queryLengthLimit: number;
|
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>(
|
readOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
entity: E,
|
entity: E,
|
||||||
id: PrimaryFieldType,
|
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>(
|
readMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
entity: E,
|
entity: E,
|
||||||
query: RepoQueryIn = {},
|
query: RepoQueryIn = {},
|
||||||
@@ -63,25 +82,64 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
|||||||
return this.post<RepositoryResponse<Data>>(["entity", entity as any], input);
|
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>(
|
updateOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
entity: E,
|
entity: E,
|
||||||
id: PrimaryFieldType,
|
id: PrimaryFieldType,
|
||||||
input: Partial<Omit<Data, "id">>,
|
input: Partial<Omit<Data, "id">>,
|
||||||
) {
|
) {
|
||||||
|
if (!id) throw new Error("ID is required");
|
||||||
return this.patch<RepositoryResponse<Data>>(["entity", entity as any, id], input);
|
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>(
|
deleteOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||||
entity: E,
|
entity: E,
|
||||||
id: PrimaryFieldType,
|
id: PrimaryFieldType,
|
||||||
) {
|
) {
|
||||||
|
if (!id) throw new Error("ID is required");
|
||||||
return this.delete<RepositoryResponse<Data>>(["entity", entity as any, id]);
|
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"] = {}) {
|
count<E extends keyof DB | string>(entity: E, where: RepoQueryIn["where"] = {}) {
|
||||||
return this.post<RepositoryResponse<{ entity: E; count: number }>>(
|
return this.post<RepositoryResponse<{ entity: E; count: number }>>(
|
||||||
["entity", entity as any, "fn", "count"],
|
["entity", entity as any, "fn", "count"],
|
||||||
where,
|
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",
|
"/:entity",
|
||||||
permission(DataPermissions.entityCreate),
|
permission(DataPermissions.entityCreate),
|
||||||
tb("param", Type.Object({ entity: Type.String() })),
|
tb("param", Type.Object({ entity: Type.String() })),
|
||||||
|
tb("json", Type.Union([Type.Object({}), Type.Array(Type.Object({}))])),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { entity } = c.req.param();
|
const { entity } = c.req.param();
|
||||||
if (!this.entityExists(entity)) {
|
if (!this.entityExists(entity)) {
|
||||||
return this.notFound(c);
|
return this.notFound(c);
|
||||||
}
|
}
|
||||||
const body = (await c.req.json()) as EntityData;
|
const body = (await c.req.json()) as EntityData | EntityData[];
|
||||||
const result = await this.em.mutator(entity).insertOne(body);
|
|
||||||
|
|
||||||
|
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);
|
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)
|
// @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;
|
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(
|
const qb = this.appendWhere(this.conn.deleteFrom(entity.name), where).returning(
|
||||||
entity.getSelect(),
|
entity.getSelect(),
|
||||||
);
|
);
|
||||||
@@ -282,11 +287,16 @@ export class Mutator<
|
|||||||
|
|
||||||
async updateWhere(
|
async updateWhere(
|
||||||
data: Partial<Input>,
|
data: Partial<Input>,
|
||||||
where?: RepoQuery["where"],
|
where: RepoQuery["where"],
|
||||||
): Promise<MutatorResponse<Output[]>> {
|
): Promise<MutatorResponse<Output[]>> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
const validatedData = await this.getValidatedData(data, "update");
|
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)
|
const query = this.appendWhere(this.conn.updateTable(entity.name), where)
|
||||||
.set(validatedData as any)
|
.set(validatedData as any)
|
||||||
.returning(entity.getSelect());
|
.returning(entity.getSelect());
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
type BooleanLike,
|
type BooleanLike,
|
||||||
type FilterQuery,
|
type FilterQuery,
|
||||||
type Primitive,
|
type Primitive,
|
||||||
type TExpression,
|
|
||||||
exp,
|
exp,
|
||||||
isBooleanLike,
|
isBooleanLike,
|
||||||
isPrimitive,
|
isPrimitive,
|
||||||
|
|||||||
@@ -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, {
|
return this.request<Data>(_input, undefined, {
|
||||||
..._init,
|
..._init,
|
||||||
|
body,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -171,7 +172,7 @@ export function createResponseProxy<Body = any, Data = any>(
|
|||||||
body: Body,
|
body: Body,
|
||||||
data?: Data,
|
data?: Data,
|
||||||
): ResponseObject<Body, 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"];
|
const _props = ["raw", "body", "ok", "status", "res", "data", "toJSON"];
|
||||||
|
|
||||||
// that's okay, since you have to check res.ok anyway
|
// 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") {
|
if (prop === "toJSON") {
|
||||||
return () => target;
|
return () => target;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Reflect.get(target, prop, receiver);
|
return Reflect.get(target, prop, receiver);
|
||||||
},
|
},
|
||||||
has(target, prop) {
|
has(target, prop) {
|
||||||
@@ -223,12 +225,19 @@ export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
|||||||
fetcher?: typeof fetch;
|
fetcher?: typeof fetch;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
},
|
},
|
||||||
|
protected refineData?: (data: T) => any,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get verbose() {
|
get verbose() {
|
||||||
return this.options?.verbose ?? false;
|
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>> {
|
async execute(): Promise<ResponseObject<T>> {
|
||||||
// delay in dev environment
|
// delay in dev environment
|
||||||
isDebug() && (await new Promise((resolve) => setTimeout(resolve, 200)));
|
isDebug() && (await new Promise((resolve) => setTimeout(resolve, 200)));
|
||||||
@@ -265,6 +274,15 @@ export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
|||||||
resBody = res.body;
|
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);
|
return createResponseProxy<T>(res, resBody, resData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,14 +88,32 @@ const { data } = await api.data.readMany("posts", {
|
|||||||
limit: 10,
|
limit: 10,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
select: ["id", "title", "views"],
|
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: {
|
where: {
|
||||||
|
// same as '{ title: { $eg: "Hello, World!" } }'
|
||||||
title: "Hello, World!",
|
title: "Hello, World!",
|
||||||
|
// only with views greater than 100
|
||||||
views: {
|
views: {
|
||||||
$gt: 100
|
$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.
|
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])`
|
### `data.updateOne([entity], [id], [data])`
|
||||||
To update a single record of an entity, use the `updateOne` method:
|
To update a single record of an entity, use the `updateOne` method:
|
||||||
```ts
|
```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])`
|
### `data.deleteOne([entity], [id])`
|
||||||
To delete a single record of an entity, use the `deleteOne` method:
|
To delete a single record of an entity, use the `deleteOne` method:
|
||||||
```ts
|
```ts
|
||||||
const { data } = await api.data.deleteOne("posts", 1);
|
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`)
|
## Auth (`api.auth`)
|
||||||
Access the `Auth` specific API methods at `api.auth`. If there is successful authentication, the
|
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.
|
API will automatically save the token and use it for subsequent requests.
|
||||||
|
|||||||
Reference in New Issue
Block a user