Merge pull request #104 from bknd-io/feat/additional-data-api-methods

feat/additional-data-api-methods
This commit is contained in:
dswbx
2025-03-05 08:24:19 +01:00
committed by GitHub
13 changed files with 307 additions and 36 deletions

View File

@@ -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();
} }
}); });
}); });

View File

@@ -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
}); });

View File

@@ -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);

View File

@@ -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(

View File

@@ -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": {

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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,
);
}
} }

View File

@@ -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);
}, },
); );

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) // @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());

View File

@@ -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,

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, { 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);
} }

View File

@@ -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.