mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge pull request #83 from bknd-io/refactor/data-api-entity-prefix
prefixed data entity endpoints with /entity
This commit is contained in:
@@ -17,11 +17,11 @@ describe("DataApi", () => {
|
||||
|
||||
const get = api.readMany("a".repeat(300), { select: ["id", "name"] });
|
||||
expect(get.request.method).toBe("GET");
|
||||
expect(new URL(get.request.url).pathname).toBe(`/api/data/${"a".repeat(300)}`);
|
||||
expect(new URL(get.request.url).pathname).toBe(`/api/data/entity/${"a".repeat(300)}`);
|
||||
|
||||
const post = api.readMany("a".repeat(1000), { select: ["id", "name"] });
|
||||
expect(post.request.method).toBe("POST");
|
||||
expect(new URL(post.request.url).pathname).toBe(`/api/data/${"a".repeat(1000)}/query`);
|
||||
expect(new URL(post.request.url).pathname).toBe(`/api/data/entity/${"a".repeat(1000)}/query`);
|
||||
});
|
||||
|
||||
it("returns result", async () => {
|
||||
@@ -39,7 +39,7 @@ describe("DataApi", () => {
|
||||
const app = controller.getController();
|
||||
|
||||
{
|
||||
const res = (await app.request("/posts")) as Response;
|
||||
const res = (await app.request("/entity/posts")) as Response;
|
||||
const { data } = await res.json();
|
||||
expect(data.length).toEqual(3);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ describe("[data] DataController", async () => {
|
||||
//console.log("app.routes", app.routes);
|
||||
// create users
|
||||
for await (const _user of fixtures.users) {
|
||||
const res = await app.request("/users", {
|
||||
const res = await app.request("/entity/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_user)
|
||||
});
|
||||
@@ -131,7 +131,7 @@ describe("[data] DataController", async () => {
|
||||
|
||||
// create posts
|
||||
for await (const _post of fixtures.posts) {
|
||||
const res = await app.request("/posts", {
|
||||
const res = await app.request("/entity/posts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(_post)
|
||||
});
|
||||
@@ -145,7 +145,7 @@ describe("[data] DataController", async () => {
|
||||
});
|
||||
|
||||
test("/:entity (read many)", async () => {
|
||||
const res = await app.request("/users");
|
||||
const res = await app.request("/entity/users");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
@@ -156,7 +156,7 @@ describe("[data] DataController", async () => {
|
||||
});
|
||||
|
||||
test("/:entity/query (func query)", async () => {
|
||||
const res = await app.request("/users/query", {
|
||||
const res = await app.request("/entity/users/query", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
@@ -175,7 +175,7 @@ describe("[data] DataController", async () => {
|
||||
});
|
||||
|
||||
test("/:entity (read many, paginated)", async () => {
|
||||
const res = await app.request("/users?limit=1&offset=2");
|
||||
const res = await app.request("/entity/users?limit=1&offset=2");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
|
||||
expect(data.meta.total).toBe(3);
|
||||
@@ -186,7 +186,7 @@ describe("[data] DataController", async () => {
|
||||
});
|
||||
|
||||
test("/:entity/:id (read one)", async () => {
|
||||
const res = await app.request("/users/3");
|
||||
const res = await app.request("/entity/users/3");
|
||||
const data = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
console.log("data", data);
|
||||
|
||||
@@ -197,7 +197,7 @@ describe("[data] DataController", async () => {
|
||||
});
|
||||
|
||||
test("/:entity (update one)", async () => {
|
||||
const res = await app.request("/users/3", {
|
||||
const res = await app.request("/entity/users/3", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ name: "new name" })
|
||||
});
|
||||
@@ -208,7 +208,7 @@ describe("[data] DataController", async () => {
|
||||
});
|
||||
|
||||
test("/:entity/:id/:reference (read references)", async () => {
|
||||
const res = await app.request("/users/1/posts");
|
||||
const res = await app.request("/entity/users/1/posts");
|
||||
const data = (await res.json()) as RepositoryResponse;
|
||||
console.log("data", data);
|
||||
|
||||
@@ -220,14 +220,14 @@ describe("[data] DataController", async () => {
|
||||
});
|
||||
|
||||
test("/:entity/:id (delete one)", async () => {
|
||||
const res = await app.request("/posts/2", {
|
||||
const res = await app.request("/entity/posts/2", {
|
||||
method: "DELETE"
|
||||
});
|
||||
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
|
||||
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
|
||||
|
||||
// verify
|
||||
const res2 = await app.request("/posts");
|
||||
const res2 = await app.request("/entity/posts");
|
||||
const data2 = (await res2.json()) as RepositoryResponse;
|
||||
expect(data2.meta.total).toBe(1);
|
||||
});
|
||||
|
||||
24
app/__test__/integration/config.integration.test.ts
Normal file
24
app/__test__/integration/config.integration.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { createApp } from "../../src";
|
||||
import { Api } from "../../src/Api";
|
||||
|
||||
describe("integration config", () => {
|
||||
it("should create an entity", async () => {
|
||||
const app = createApp();
|
||||
await app.build();
|
||||
const api = new Api({
|
||||
host: "http://localhost",
|
||||
fetcher: app.server.request as typeof fetch
|
||||
});
|
||||
|
||||
// create entity
|
||||
await api.system.addConfig("data", "entities.posts", {
|
||||
name: "posts",
|
||||
config: { sort_field: "id", sort_dir: "asc" },
|
||||
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
|
||||
type: "regular"
|
||||
});
|
||||
|
||||
expect(app.em.entities.map((e) => e.name)).toContain("posts");
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ export type ApiOptions = {
|
||||
key?: string;
|
||||
localStorage?: boolean;
|
||||
fetcher?: typeof fetch;
|
||||
verbose?: boolean;
|
||||
verified?: boolean;
|
||||
} & (
|
||||
| {
|
||||
@@ -196,7 +197,8 @@ export class Api {
|
||||
host: this.baseUrl,
|
||||
token: this.token,
|
||||
headers: this.options.headers,
|
||||
token_transport: this.token_transport
|
||||
token_transport: this.token_transport,
|
||||
verbose: this.options.verbose
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,6 @@ export class AuthController extends Controller {
|
||||
return c.json({ strategies, basepath });
|
||||
});
|
||||
|
||||
return hono;
|
||||
return hono.all("*", (c) => c.notFound());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
id: PrimaryFieldType,
|
||||
query: Omit<RepoQueryIn, "where" | "limit" | "offset"> = {}
|
||||
) {
|
||||
return this.get<Pick<RepositoryResponse<Data>, "meta" | "data">>([entity as any, id], query);
|
||||
return this.get<Pick<RepositoryResponse<Data>, "meta" | "data">>(
|
||||
["entity", entity as any, id],
|
||||
query
|
||||
);
|
||||
}
|
||||
|
||||
readMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
@@ -33,13 +36,13 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
type T = Pick<RepositoryResponse<Data[]>, "meta" | "data">;
|
||||
|
||||
const input = query ?? this.options.defaultQuery;
|
||||
const req = this.get<T>([entity as any], input);
|
||||
const req = this.get<T>(["entity", entity as any], input);
|
||||
|
||||
if (req.request.url.length <= this.options.queryLengthLimit) {
|
||||
return req;
|
||||
}
|
||||
|
||||
return this.post<T>([entity as any, "query"], input);
|
||||
return this.post<T>(["entity", entity as any, "query"], input);
|
||||
}
|
||||
|
||||
readManyByReference<
|
||||
@@ -48,7 +51,7 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
Data = R extends keyof DB ? DB[R] : EntityData
|
||||
>(entity: E, id: PrimaryFieldType, reference: R, query: RepoQueryIn = {}) {
|
||||
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
|
||||
[entity as any, id, reference],
|
||||
["entity", entity as any, id, reference],
|
||||
query ?? this.options.defaultQuery
|
||||
);
|
||||
}
|
||||
@@ -57,7 +60,7 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
entity: E,
|
||||
input: Omit<Data, "id">
|
||||
) {
|
||||
return this.post<RepositoryResponse<Data>>([entity as any], input);
|
||||
return this.post<RepositoryResponse<Data>>(["entity", entity as any], input);
|
||||
}
|
||||
|
||||
updateOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
@@ -65,19 +68,19 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
id: PrimaryFieldType,
|
||||
input: Partial<Omit<Data, "id">>
|
||||
) {
|
||||
return this.patch<RepositoryResponse<Data>>([entity as any, id], input);
|
||||
return this.patch<RepositoryResponse<Data>>(["entity", entity as any, id], input);
|
||||
}
|
||||
|
||||
deleteOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
id: PrimaryFieldType
|
||||
) {
|
||||
return this.delete<RepositoryResponse<Data>>([entity as any, id]);
|
||||
return this.delete<RepositoryResponse<Data>>(["entity", entity as any, id]);
|
||||
}
|
||||
|
||||
count<E extends keyof DB | string>(entity: E, where: RepoQuery["where"] = {}) {
|
||||
return this.post<RepositoryResponse<{ entity: E; count: number }>>(
|
||||
[entity as any, "fn", "count"],
|
||||
["entity", entity as any, "fn", "count"],
|
||||
where
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,44 +109,7 @@ export class DataController extends Controller {
|
||||
});
|
||||
|
||||
/**
|
||||
* Function endpoints
|
||||
*/
|
||||
hono
|
||||
// fn: count
|
||||
.post(
|
||||
"/:entity/fn/count",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
const where = c.req.json() as any;
|
||||
const result = await this.em.repository(entity).count(where);
|
||||
return c.json({ entity, count: result.count });
|
||||
}
|
||||
)
|
||||
// fn: exists
|
||||
.post(
|
||||
"/:entity/fn/exists",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
const where = c.req.json() as any;
|
||||
const result = await this.em.repository(entity).exists(where);
|
||||
return c.json({ entity, exists: result.exists });
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Read endpoints
|
||||
* Schema endpoints
|
||||
*/
|
||||
hono
|
||||
// read entity schema
|
||||
@@ -197,7 +160,64 @@ export class DataController extends Controller {
|
||||
...schema
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// entity endpoints
|
||||
hono.route("/entity", this.getEntityRoutes());
|
||||
|
||||
return hono.all("*", (c) => c.notFound());
|
||||
}
|
||||
|
||||
private getEntityRoutes() {
|
||||
const { permission } = this.middlewares;
|
||||
const hono = this.create();
|
||||
|
||||
const definedEntities = this.em.entities.map((e) => e.name);
|
||||
const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" }))
|
||||
.Decode(Number.parseInt)
|
||||
.Encode(String);
|
||||
|
||||
/**
|
||||
* Function endpoints
|
||||
*/
|
||||
hono
|
||||
// fn: count
|
||||
.post(
|
||||
"/:entity/fn/count",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
const where = c.req.json() as any;
|
||||
const result = await this.em.repository(entity).count(where);
|
||||
return c.json({ entity, count: result.count });
|
||||
}
|
||||
)
|
||||
// fn: exists
|
||||
.post(
|
||||
"/:entity/fn/exists",
|
||||
permission(DataPermissions.entityRead),
|
||||
tb("param", Type.Object({ entity: Type.String() })),
|
||||
async (c) => {
|
||||
const { entity } = c.req.valid("param");
|
||||
if (!this.entityExists(entity)) {
|
||||
return c.notFound();
|
||||
}
|
||||
|
||||
const where = c.req.json() as any;
|
||||
const result = await this.em.repository(entity).exists(where);
|
||||
return c.json({ entity, exists: result.exists });
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Read endpoints
|
||||
*/
|
||||
hono
|
||||
// read many
|
||||
.get(
|
||||
"/:entity",
|
||||
|
||||
@@ -214,6 +214,6 @@ export class MediaController extends Controller {
|
||||
}
|
||||
);
|
||||
|
||||
return hono;
|
||||
return hono.all("*", (c) => c.notFound());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export type BaseModuleApiOptions = {
|
||||
token?: string;
|
||||
headers?: Headers;
|
||||
token_transport?: "header" | "cookie" | "none";
|
||||
verbose?: boolean;
|
||||
};
|
||||
|
||||
/** @deprecated */
|
||||
@@ -107,7 +108,8 @@ export abstract class ModuleApi<Options extends BaseModuleApiOptions = BaseModul
|
||||
});
|
||||
|
||||
return new FetchPromise(request, {
|
||||
fetcher: this.fetcher
|
||||
fetcher: this.fetcher,
|
||||
verbose: this.options.verbose
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,15 +221,35 @@ export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
||||
public request: Request,
|
||||
protected options?: {
|
||||
fetcher?: typeof fetch;
|
||||
verbose?: boolean;
|
||||
}
|
||||
) {}
|
||||
|
||||
get verbose() {
|
||||
return this.options?.verbose ?? false;
|
||||
}
|
||||
|
||||
async execute(): Promise<ResponseObject<T>> {
|
||||
// delay in dev environment
|
||||
isDebug() && (await new Promise((resolve) => setTimeout(resolve, 200)));
|
||||
|
||||
const fetcher = this.options?.fetcher ?? fetch;
|
||||
if (this.verbose) {
|
||||
console.log("[FetchPromise] Request", {
|
||||
method: this.request.method,
|
||||
url: this.request.url
|
||||
});
|
||||
}
|
||||
|
||||
const res = await fetcher(this.request);
|
||||
if (this.verbose) {
|
||||
console.log("[FetchPromise] Response", {
|
||||
res: res,
|
||||
ok: res.ok,
|
||||
status: res.status
|
||||
});
|
||||
}
|
||||
|
||||
let resBody: any;
|
||||
let resData: any;
|
||||
|
||||
@@ -242,6 +264,7 @@ export class FetchPromise<T = ApiResponse<any>> implements Promise<T> {
|
||||
} else {
|
||||
resBody = res.body;
|
||||
}
|
||||
console.groupEnd();
|
||||
|
||||
return createResponseProxy<T>(res, resBody, resData);
|
||||
}
|
||||
|
||||
@@ -556,7 +556,9 @@ export class ModuleManager {
|
||||
await this.options?.onFirstBoot?.();
|
||||
}
|
||||
|
||||
mutateConfigSafe<Module extends keyof Modules>(name: Module) {
|
||||
mutateConfigSafe<Module extends keyof Modules>(
|
||||
name: Module
|
||||
): Pick<ReturnType<Modules[Module]["schema"]>, "set" | "patch" | "overwrite" | "remove"> {
|
||||
const module = this.modules[name];
|
||||
const copy = structuredClone(this.configs());
|
||||
|
||||
|
||||
@@ -142,14 +142,13 @@ export class SystemController extends Controller {
|
||||
const value = await c.req.json();
|
||||
const path = c.req.param("path") as string;
|
||||
|
||||
const moduleConfig = this.app.mutateConfig(module);
|
||||
if (moduleConfig.has(path)) {
|
||||
if (this.app.modules.get(module).schema().has(path)) {
|
||||
return c.json({ success: false, path, error: "Path already exists" }, { status: 400 });
|
||||
}
|
||||
console.log("-- add", module, path, value);
|
||||
|
||||
return await handleConfigUpdateResponse(c, async () => {
|
||||
await moduleConfig.patch(path, value);
|
||||
await this.app.mutateConfig(module).patch(path, value);
|
||||
return {
|
||||
success: true,
|
||||
module,
|
||||
@@ -283,6 +282,6 @@ export class SystemController extends Controller {
|
||||
return c.json(generateOpenAPI(config));
|
||||
});
|
||||
|
||||
return hono;
|
||||
return hono.all("*", (c) => c.notFound());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ function dataRoutes(config: ModuleConfigs): { paths: OAS.Document["paths"] } {
|
||||
|
||||
const tags = ["data"];
|
||||
const paths: OAS.PathsObject = {
|
||||
"/{entity}": {
|
||||
"/entity/{entity}": {
|
||||
get: {
|
||||
summary: "List entities",
|
||||
parameters: [params.entity],
|
||||
@@ -148,7 +148,7 @@ function dataRoutes(config: ModuleConfigs): { paths: OAS.Document["paths"] } {
|
||||
tags
|
||||
}
|
||||
},
|
||||
"/{entity}/{id}": {
|
||||
"/entity/{entity}/{id}": {
|
||||
get: {
|
||||
summary: "Get entity",
|
||||
parameters: [params.entity, params.entityId],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Api, type ApiOptions, type TApiUser } from "Api";
|
||||
import { isDebug } from "core";
|
||||
import type { AppTheme } from "modules/server/AppServer";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
@@ -32,7 +33,7 @@ export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps)
|
||||
}
|
||||
|
||||
//console.log("api init", { host: actualBaseUrl, user: user ?? winCtx.user });
|
||||
const api = new Api({ host: actualBaseUrl, user: user ?? winCtx.user });
|
||||
const api = new Api({ host: actualBaseUrl, user: user ?? winCtx.user, verbose: isDebug() });
|
||||
|
||||
return (
|
||||
<ClientContext.Provider value={{ baseUrl: api.baseUrl, api }}>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"tags": ["system"]
|
||||
}
|
||||
},
|
||||
"/api/data/{entity}": {
|
||||
"/api/data/entity/{entity}": {
|
||||
"get": {
|
||||
"summary": "List entities",
|
||||
"parameters": [
|
||||
@@ -155,7 +155,7 @@
|
||||
"tags": ["data"]
|
||||
}
|
||||
},
|
||||
"/api/data/{entity}/{id}": {
|
||||
"/api/data/entity/{entity}/{id}": {
|
||||
"get": {
|
||||
"summary": "Get entity",
|
||||
"parameters": [
|
||||
|
||||
Reference in New Issue
Block a user