mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3f95f9552 | |||
| 372f94d22a | |||
| d6f94a2ce1 | |||
| 89a39a7dc6 | |||
| 88cf65f792 |
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Entity, NumberField, TextField } from "../../../src/data";
|
||||
import { Entity, NumberField, TextField } from "data";
|
||||
import * as p from "data/prototype";
|
||||
|
||||
describe("[data] Entity", async () => {
|
||||
const entity = new Entity("test", [
|
||||
@@ -47,14 +48,7 @@ describe("[data] Entity", async () => {
|
||||
expect(entity.getField("new_field")).toBe(field);
|
||||
});
|
||||
|
||||
// @todo: move this to ClientApp
|
||||
/*test("serialize and deserialize", async () => {
|
||||
const json = entity.toJSON();
|
||||
//sconsole.log("json", json.fields);
|
||||
const newEntity = Entity.deserialize(json);
|
||||
//console.log("newEntity", newEntity.toJSON().fields);
|
||||
expect(newEntity).toBeInstanceOf(Entity);
|
||||
expect(json).toEqual(newEntity.toJSON());
|
||||
expect(json.fields).toEqual(newEntity.toJSON().fields);
|
||||
});*/
|
||||
test.only("types", async () => {
|
||||
console.log(entity.toTypes());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,8 +47,8 @@ describe("[data] EntityManager", async () => {
|
||||
em.addRelation(new ManyToOneRelation(posts, users));
|
||||
expect(em.relations.all.length).toBe(1);
|
||||
expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation);
|
||||
expect(em.relationsOf("users")).toEqual([em.relations.all[0]]);
|
||||
expect(em.relationsOf("posts")).toEqual([em.relations.all[0]]);
|
||||
expect(em.relationsOf("users")).toEqual([em.relations.all[0]!]);
|
||||
expect(em.relationsOf("posts")).toEqual([em.relations.all[0]!]);
|
||||
expect(em.hasRelations("users")).toBe(true);
|
||||
expect(em.hasRelations("posts")).toBe(true);
|
||||
expect(em.relatedEntitiesOf("users")).toEqual([posts]);
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"bin": "./dist/cli/index.js",
|
||||
"version": "0.11.2",
|
||||
"version": "0.12.0",
|
||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||
"homepage": "https://bknd.io",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Authenticator, AuthPermissions, Role, type Strategy } from "auth";
|
||||
import type { PasswordStrategy } from "auth/authenticate/strategies";
|
||||
import { $console, type DB, type PrimaryFieldType } from "core";
|
||||
import { $console, type DB } from "core";
|
||||
import { secureRandomString, transformObject } from "core/utils";
|
||||
import type { Entity, EntityManager } from "data";
|
||||
import { em, entity, enumm, type FieldSchema, text } from "data/prototype";
|
||||
@@ -8,11 +8,13 @@ import { Module } from "modules/Module";
|
||||
import { AuthController } from "./api/AuthController";
|
||||
import { type AppAuthSchema, authConfigSchema, STRATEGIES } from "./auth-schema";
|
||||
import { AppUserPool } from "auth/AppUserPool";
|
||||
import type { AppEntity } from "core/config";
|
||||
|
||||
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
|
||||
declare module "core" {
|
||||
interface Users extends AppEntity, UserFieldSchema {}
|
||||
interface DB {
|
||||
users: { id: PrimaryFieldType } & UserFieldSchema;
|
||||
users: Users;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,3 +5,4 @@ export { debug } from "./debug";
|
||||
export { user } from "./user";
|
||||
export { create } from "./create";
|
||||
export { copyAssets } from "./copy-assets";
|
||||
export { types } from "./types";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { CliCommand } from "cli/types";
|
||||
import { Option } from "commander";
|
||||
import { makeAppFromEnv } from "cli/commands/run";
|
||||
import { EntityTypescript } from "data/entities/EntityTypescript";
|
||||
import { writeFile } from "cli/utils/sys";
|
||||
import c from "picocolors";
|
||||
|
||||
export const types: CliCommand = (program) => {
|
||||
program
|
||||
.command("types")
|
||||
.description("generate types")
|
||||
.addOption(new Option("-o, --outfile <outfile>", "output file").default("bknd-types.d.ts"))
|
||||
.addOption(new Option("--no-write", "do not write to file").default(true))
|
||||
.action(action);
|
||||
};
|
||||
|
||||
async function action({
|
||||
outfile,
|
||||
write,
|
||||
}: {
|
||||
outfile: string;
|
||||
write: boolean;
|
||||
}) {
|
||||
const app = await makeAppFromEnv({
|
||||
server: "node",
|
||||
});
|
||||
await app.build();
|
||||
|
||||
const et = new EntityTypescript(app.em);
|
||||
|
||||
if (write) {
|
||||
await writeFile(outfile, et.toString());
|
||||
console.info(`\nTypes written to ${c.cyan(outfile)}`);
|
||||
} else {
|
||||
console.info(et.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { $console } from "core";
|
||||
import { execSync, exec as nodeExec } from "node:child_process";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { readFile, writeFile as nodeWriteFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
|
||||
@@ -48,6 +49,16 @@ export async function fileExists(filePath: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeFile(filePath: string, content: string) {
|
||||
try {
|
||||
await nodeWriteFile(path.resolve(process.cwd(), filePath), content);
|
||||
return true;
|
||||
} catch (e) {
|
||||
$console.error("Failed to write file", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function exec(command: string, opts?: { silent?: boolean; env?: Record<string, string> }) {
|
||||
const stdio = opts?.silent ? "pipe" : "inherit";
|
||||
const output = execSync(command, {
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
*/
|
||||
import type { Generated } from "kysely";
|
||||
|
||||
export type PrimaryFieldType = number | Generated<number>;
|
||||
export type PrimaryFieldType<IdType extends number = number> = IdType | Generated<IdType>;
|
||||
|
||||
export interface AppEntity<IdType extends number = number> {
|
||||
id: PrimaryFieldType<IdType>;
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
// make sure to make unknown as "any"
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Hono, MiddlewareHandler } from "hono";
|
||||
export { tbValidator } from "./server/lib/tbValidator";
|
||||
export { Exception, BkndError } from "./errors";
|
||||
export { isDebug, env } from "./env";
|
||||
export { type PrimaryFieldType, config, type DB } from "./config";
|
||||
export { type PrimaryFieldType, config, type DB, type AppEntity } from "./config";
|
||||
export { AwsClient } from "./clients/aws/AwsClient";
|
||||
export {
|
||||
SimpleRenderer,
|
||||
|
||||
@@ -359,3 +359,50 @@ export function getPath(
|
||||
throw new Error(`Invalid path: ${path.join(".")}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function objectToJsLiteral(value: object, indent: number = 0, _level: number = 0): string {
|
||||
const nl = indent ? "\n" : "";
|
||||
const pad = (lvl: number) => (indent ? " ".repeat(indent * lvl) : "");
|
||||
const openPad = pad(_level + 1);
|
||||
const closePad = pad(_level);
|
||||
|
||||
// primitives
|
||||
if (value === null) return "null";
|
||||
if (value === undefined) return "undefined";
|
||||
const t = typeof value;
|
||||
if (t === "string") return JSON.stringify(value); // handles escapes
|
||||
if (t === "number" || t === "boolean") return String(value);
|
||||
|
||||
// arrays
|
||||
if (Array.isArray(value)) {
|
||||
const out = value
|
||||
.map((v) => objectToJsLiteral(v, indent, _level + 1))
|
||||
.join(", " + (indent ? nl + openPad : ""));
|
||||
return (
|
||||
"[" +
|
||||
(indent && value.length ? nl + openPad : "") +
|
||||
out +
|
||||
(indent && value.length ? nl + closePad : "") +
|
||||
"]"
|
||||
);
|
||||
}
|
||||
|
||||
// objects
|
||||
if (t === "object") {
|
||||
const entries = Object.entries(value).map(([k, v]) => {
|
||||
const idOk = /^[A-Za-z_$][\w$]*$/.test(k); // valid identifier?
|
||||
const key = idOk ? k : JSON.stringify(k); // quote if needed
|
||||
return key + ": " + objectToJsLiteral(v, indent, _level + 1);
|
||||
});
|
||||
const out = entries.join(", " + (indent ? nl + openPad : ""));
|
||||
return (
|
||||
"{" +
|
||||
(indent && entries.length ? nl + openPad : "") +
|
||||
out +
|
||||
(indent && entries.length ? nl + closePad : "") +
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
throw new TypeError(`Unsupported data type: ${t}`);
|
||||
}
|
||||
|
||||
+30
-27
@@ -1,5 +1,6 @@
|
||||
import type { DB } from "core";
|
||||
import type { EntityData, RepoQueryIn, RepositoryResponse } from "data";
|
||||
import type { Insertable, Selectable, Updateable } from "kysely";
|
||||
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules";
|
||||
import type { FetchPromise, ResponseObject } from "modules/ModuleApi";
|
||||
|
||||
@@ -25,21 +26,23 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
}
|
||||
}
|
||||
|
||||
readOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
readOne<E extends keyof DB | string>(
|
||||
entity: E,
|
||||
id: PrimaryFieldType,
|
||||
query: Omit<RepoQueryIn, "where" | "limit" | "offset"> = {},
|
||||
) {
|
||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||
return this.get<Pick<RepositoryResponse<Data>, "meta" | "data">>(
|
||||
["entity", entity as any, id],
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
readOneBy<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
readOneBy<E extends keyof DB | string>(
|
||||
entity: E,
|
||||
query: Omit<RepoQueryIn, "limit" | "offset" | "sort"> = {},
|
||||
) {
|
||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||
type T = Pick<RepositoryResponse<Data>, "meta" | "data">;
|
||||
return this.readMany(entity, {
|
||||
...query,
|
||||
@@ -48,10 +51,8 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
}).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 = {},
|
||||
) {
|
||||
readMany<E extends keyof DB | string>(entity: E, query: RepoQueryIn = {}) {
|
||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||
type T = Pick<RepositoryResponse<Data[]>, "meta" | "data">;
|
||||
|
||||
const input = query ?? this.options.defaultQuery;
|
||||
@@ -64,68 +65,70 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
||||
return this.post<T>(["entity", entity as any, "query"], input);
|
||||
}
|
||||
|
||||
readManyByReference<
|
||||
E extends keyof DB | string,
|
||||
R extends keyof DB | string,
|
||||
Data = R extends keyof DB ? DB[R] : EntityData,
|
||||
>(entity: E, id: PrimaryFieldType, reference: R, query: RepoQueryIn = {}) {
|
||||
readManyByReference<E extends keyof DB | string, R extends keyof DB | string>(
|
||||
entity: E,
|
||||
id: PrimaryFieldType,
|
||||
reference: R,
|
||||
query: RepoQueryIn = {},
|
||||
) {
|
||||
type Data = R extends keyof DB ? Selectable<DB[R]> : EntityData;
|
||||
return this.get<Pick<RepositoryResponse<Data[]>, "meta" | "data">>(
|
||||
["entity", entity as any, id, reference],
|
||||
query ?? this.options.defaultQuery,
|
||||
);
|
||||
}
|
||||
|
||||
createOne<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
createOne<E extends keyof DB | string, Input = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
input: Omit<Data, "id">,
|
||||
input: Insertable<Input>,
|
||||
) {
|
||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||
return this.post<RepositoryResponse<Data>>(["entity", entity as any], input);
|
||||
}
|
||||
|
||||
createMany<E extends keyof DB | string, Data = E extends keyof DB ? DB[E] : EntityData>(
|
||||
createMany<E extends keyof DB | string, Input = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
input: Omit<Data, "id">[],
|
||||
input: Insertable<Input>[],
|
||||
) {
|
||||
if (!input || !Array.isArray(input) || input.length === 0) {
|
||||
throw new Error("input is required");
|
||||
}
|
||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||
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, Input = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
id: PrimaryFieldType,
|
||||
input: Partial<Omit<Data, "id">>,
|
||||
input: Updateable<Input>,
|
||||
) {
|
||||
if (!id) throw new Error("ID is required");
|
||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||
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>(
|
||||
updateMany<E extends keyof DB | string, Input = E extends keyof DB ? DB[E] : EntityData>(
|
||||
entity: E,
|
||||
where: RepoQueryIn["where"],
|
||||
update: Partial<Omit<Data, "id">>,
|
||||
update: Updateable<Input>,
|
||||
) {
|
||||
this.requireObjectSet(where);
|
||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||
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,
|
||||
) {
|
||||
deleteOne<E extends keyof DB | string>(entity: E, id: PrimaryFieldType) {
|
||||
if (!id) throw new Error("ID is required");
|
||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||
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"],
|
||||
) {
|
||||
deleteMany<E extends keyof DB | string>(entity: E, where: RepoQueryIn["where"]) {
|
||||
this.requireObjectSet(where);
|
||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||
return this.delete<RepositoryResponse<Data>>(["entity", entity as any], where);
|
||||
}
|
||||
|
||||
|
||||
@@ -280,6 +280,15 @@ export class Entity<
|
||||
return options?.clean ? JSON.parse(JSON.stringify(schema)) : schema;
|
||||
}
|
||||
|
||||
toTypes() {
|
||||
return {
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
comment: this.config.description,
|
||||
fields: Object.fromEntries(this.getFields().map((field) => [field.name, field.toType()])),
|
||||
};
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type: this.type,
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import type { Entity, EntityManager, EntityRelation, TEntityType } from "data";
|
||||
import { autoFormatString } from "core/utils";
|
||||
import { AppAuth, AppMedia } from "modules";
|
||||
|
||||
export type TEntityTSType = {
|
||||
name: string;
|
||||
type: TEntityType;
|
||||
comment?: string;
|
||||
fields: Record<string, TFieldTSType>;
|
||||
};
|
||||
|
||||
// [select, insert, update]
|
||||
type TFieldContextType = boolean | [boolean, boolean, boolean];
|
||||
|
||||
export type TFieldTSType = {
|
||||
required?: TFieldContextType;
|
||||
fillable?: TFieldContextType;
|
||||
type: "PrimaryFieldType" | string;
|
||||
comment?: string;
|
||||
import?: {
|
||||
package: string;
|
||||
name: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type EntityTypescriptOptions = {
|
||||
indentWidth?: number;
|
||||
indentChar?: string;
|
||||
entityCommentMultiline?: boolean;
|
||||
fieldCommentMultiline?: boolean;
|
||||
};
|
||||
|
||||
// keep a local copy here until properties have a type
|
||||
const systemEntities = {
|
||||
users: AppAuth.usersFields,
|
||||
media: AppMedia.mediaFields,
|
||||
};
|
||||
|
||||
export class EntityTypescript {
|
||||
constructor(
|
||||
protected em: EntityManager,
|
||||
protected _options: EntityTypescriptOptions = {},
|
||||
) {}
|
||||
|
||||
get options() {
|
||||
return {
|
||||
...this._options,
|
||||
indentWidth: 2,
|
||||
indentChar: " ",
|
||||
entityCommentMultiline: true,
|
||||
fieldCommentMultiline: false,
|
||||
};
|
||||
}
|
||||
|
||||
toTypes() {
|
||||
return this.em.entities.map((e) => e.toTypes());
|
||||
}
|
||||
|
||||
protected getTab(count = 1) {
|
||||
return this.options.indentChar.repeat(this.options.indentWidth).repeat(count);
|
||||
}
|
||||
|
||||
collectImports(
|
||||
type: TEntityTSType,
|
||||
imports: Record<string, string[]> = {},
|
||||
): Record<string, string[]> {
|
||||
for (const [, entity_type] of Object.entries(type.fields)) {
|
||||
for (const imp of entity_type.import ?? []) {
|
||||
const name = imp.name;
|
||||
const pkg = imp.package;
|
||||
if (!imports[pkg]) {
|
||||
imports[pkg] = [];
|
||||
}
|
||||
if (!imports[pkg].includes(name)) {
|
||||
imports[pkg].push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return imports;
|
||||
}
|
||||
|
||||
typeName(name: string) {
|
||||
return autoFormatString(name);
|
||||
}
|
||||
|
||||
fieldTypesToString(type: TEntityTSType, opts?: { ignore_fields?: string[]; indent?: number }) {
|
||||
let string = "";
|
||||
const coment_multiline = this.options.fieldCommentMultiline;
|
||||
const indent = opts?.indent ?? 1;
|
||||
for (const [field_name, field_type] of Object.entries(type.fields)) {
|
||||
if (opts?.ignore_fields?.includes(field_name)) continue;
|
||||
|
||||
let f = "";
|
||||
f += this.commentString(field_type.comment, indent, coment_multiline);
|
||||
f += `${this.getTab(indent)}${field_name}${field_type.required ? "" : "?"}: `;
|
||||
f += field_type.type + ";";
|
||||
f += "\n";
|
||||
string += f;
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
relationToFieldType(relation: EntityRelation, entity: Entity) {
|
||||
const other = relation.other(entity);
|
||||
const listable = relation.isListableFor(entity);
|
||||
const name = this.typeName(other.entity.name);
|
||||
|
||||
let type = name;
|
||||
if (other.entity.type === "system") {
|
||||
type = `DB["${other.entity.name}"]`;
|
||||
}
|
||||
|
||||
return {
|
||||
fields: {
|
||||
[other.reference]: {
|
||||
required: false,
|
||||
type: `${type}${listable ? "[]" : ""}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
importsToString(imports: Record<string, string[]>) {
|
||||
const strings: string[] = [];
|
||||
for (const [pkg, names] of Object.entries(imports)) {
|
||||
strings.push(`import type { ${names.join(", ")} } from "${pkg}";`);
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
commentString(comment?: string, indents = 0, multiline = true) {
|
||||
if (!comment) return "";
|
||||
const indent = this.getTab(indents);
|
||||
if (!multiline) return `${indent}// ${comment}\n`;
|
||||
return `${indent}/**\n${indent} * ${comment}\n${indent} */\n`;
|
||||
}
|
||||
|
||||
entityToTypeString(
|
||||
entity: Entity,
|
||||
opts?: { ignore_fields?: string[]; indent?: number; export?: boolean },
|
||||
) {
|
||||
const type = entity.toTypes();
|
||||
const name = this.typeName(type.name);
|
||||
const indent = opts?.indent ?? 1;
|
||||
const min_indent = Math.max(0, indent - 1);
|
||||
|
||||
let s = this.commentString(type.comment, min_indent, this.options.entityCommentMultiline);
|
||||
s += `${opts?.export ? "export " : ""}interface ${name} {\n`;
|
||||
s += this.fieldTypesToString(type, opts);
|
||||
|
||||
// add listable relations
|
||||
const relations = this.em.relations.relationsOf(entity);
|
||||
const rel_types = relations.map((r) =>
|
||||
this.relationToFieldType(r, entity),
|
||||
) as TEntityTSType[];
|
||||
for (const rel_type of rel_types) {
|
||||
s += this.fieldTypesToString(rel_type, {
|
||||
indent,
|
||||
});
|
||||
}
|
||||
s += `${this.getTab(min_indent)}}`;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
toString() {
|
||||
const strings: string[] = [];
|
||||
const tables: Record<string, string> = {};
|
||||
const imports: Record<string, string[]> = {
|
||||
"bknd/core": ["DB"],
|
||||
kysely: ["Insertable", "Selectable", "Updateable", "Generated"],
|
||||
};
|
||||
|
||||
// add global types
|
||||
let g = "declare global {\n";
|
||||
g += `${this.getTab(1)}type BkndEntity<T extends keyof DB> = Selectable<DB[T]>;\n`;
|
||||
g += `${this.getTab(1)}type BkndEntityCreate<T extends keyof DB> = Insertable<DB[T]>;\n`;
|
||||
g += `${this.getTab(1)}type BkndEntityUpdate<T extends keyof DB> = Updateable<DB[T]>;\n`;
|
||||
g += "}";
|
||||
strings.push(g);
|
||||
|
||||
const system_entities = this.em.entities.filter((e) => e.type === "system");
|
||||
|
||||
for (const entity of this.em.entities) {
|
||||
// skip system entities, declare addtional props in the DB interface
|
||||
if (system_entities.includes(entity)) continue;
|
||||
|
||||
const type = entity.toTypes();
|
||||
if (!type) continue;
|
||||
this.collectImports(type, imports);
|
||||
tables[type.name] = this.typeName(type.name);
|
||||
const s = this.entityToTypeString(entity, {
|
||||
export: true,
|
||||
});
|
||||
strings.push(s);
|
||||
}
|
||||
|
||||
// write tables
|
||||
let tables_string = "interface Database {\n";
|
||||
for (const [name, type] of Object.entries(tables)) {
|
||||
tables_string += `${this.getTab(1)}${name}: ${type};\n`;
|
||||
}
|
||||
tables_string += "}";
|
||||
strings.push(tables_string);
|
||||
|
||||
// merge
|
||||
let merge = `declare module "bknd/core" {\n`;
|
||||
for (const systemEntity of system_entities) {
|
||||
const system_fields = Object.keys(systemEntities[systemEntity.name]);
|
||||
const additional_fields = systemEntity.fields
|
||||
.filter((f) => !system_fields.includes(f.name) && f.type !== "primary")
|
||||
.map((f) => f.name);
|
||||
if (additional_fields.length === 0) continue;
|
||||
|
||||
merge += `${this.getTab(1)}${this.entityToTypeString(systemEntity, {
|
||||
ignore_fields: ["id", ...system_fields],
|
||||
indent: 2,
|
||||
})}\n\n`;
|
||||
}
|
||||
|
||||
merge += `${this.getTab(1)}interface DB extends Database {}\n}`;
|
||||
strings.push(merge);
|
||||
|
||||
const final = [this.importsToString(imports).join("\n"), strings.join("\n\n")];
|
||||
return final.join("\n\n");
|
||||
}
|
||||
}
|
||||
@@ -88,4 +88,11 @@ export class BooleanField<Required extends true | false = false> extends Field<
|
||||
override toJsonSchema() {
|
||||
return this.toSchemaWrapIfRequired(Type.Boolean({ default: this.getDefault() }));
|
||||
}
|
||||
|
||||
override toType() {
|
||||
return {
|
||||
...super.toType(),
|
||||
type: "boolean",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { EntityManager } from "../entities";
|
||||
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
|
||||
import { $console } from "core";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
||||
const { Type } = tbbox;
|
||||
|
||||
export const dateFieldConfigSchema = Type.Composite(
|
||||
@@ -144,4 +145,11 @@ export class DateField<Required extends true | false = false> extends Field<
|
||||
override toJsonSchema() {
|
||||
return this.toSchemaWrapIfRequired(Type.String({ default: this.getDefault() }));
|
||||
}
|
||||
|
||||
override toType(): TFieldTSType {
|
||||
return {
|
||||
...super.toType(),
|
||||
type: "Date | string",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { EntityManager } from "data";
|
||||
import { TransformPersistFailedException } from "../errors";
|
||||
import { baseFieldConfigSchema, Field, type TActionContext, type TRenderContext } from "./Field";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
||||
const { Type } = tbbox;
|
||||
|
||||
export const enumFieldConfigSchema = Type.Composite(
|
||||
@@ -140,4 +141,14 @@ export class EnumField<Required extends true | false = false, TypeOverride = str
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
override toType(): TFieldTSType {
|
||||
const union = this.getOptions().map(({ value }) =>
|
||||
typeof value === "string" ? `"${value}"` : value,
|
||||
);
|
||||
return {
|
||||
...super.toType(),
|
||||
type: union.length > 0 ? union.join(" | ") : "string",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { EntityManager } from "../entities";
|
||||
import { InvalidFieldConfigException, TransformPersistFailedException } from "../errors";
|
||||
import type { FieldSpec } from "data/connection/Connection";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
||||
const { Type } = tbbox;
|
||||
|
||||
// @todo: contexts need to be reworked
|
||||
@@ -235,6 +236,14 @@ export abstract class Field<
|
||||
return this.toSchemaWrapIfRequired(Type.Any());
|
||||
}
|
||||
|
||||
toType(): TFieldTSType {
|
||||
return {
|
||||
required: this.isRequired(),
|
||||
comment: this.getDescription(),
|
||||
type: "any",
|
||||
};
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
// @todo: current workaround because of fixed string type
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { EntityManager } from "data";
|
||||
import { TransformPersistFailedException } from "../errors";
|
||||
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
||||
const { Type } = tbbox;
|
||||
|
||||
export const jsonFieldConfigSchema = Type.Composite([baseFieldConfigSchema, Type.Object({})]);
|
||||
@@ -98,4 +99,11 @@ export class JsonField<Required extends true | false = false, TypeOverride = obj
|
||||
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
override toType(): TFieldTSType {
|
||||
return {
|
||||
...super.toType(),
|
||||
type: "any",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { type Schema as JsonSchema, Validator } from "@cfworker/json-schema";
|
||||
import { Default, FromSchema, type Static } from "core/utils";
|
||||
import { Default, FromSchema, objectToJsLiteral, type Static } from "core/utils";
|
||||
import type { EntityManager } from "data";
|
||||
import { TransformPersistFailedException } from "../errors";
|
||||
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
||||
const { Type } = tbbox;
|
||||
|
||||
export const jsonSchemaFieldConfigSchema = Type.Composite(
|
||||
@@ -121,4 +122,12 @@ export class JsonSchemaField<
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
override toType(): TFieldTSType {
|
||||
return {
|
||||
...super.toType(),
|
||||
import: [{ package: "json-schema-to-ts", name: "FromSchema" }],
|
||||
type: `FromSchema<${objectToJsLiteral(this.getJsonSchema(), 2, 1)}>`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { EntityManager } from "data";
|
||||
import { TransformPersistFailedException } from "../errors";
|
||||
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
||||
const { Type } = tbbox;
|
||||
|
||||
export const numberFieldConfigSchema = Type.Composite(
|
||||
@@ -102,4 +103,11 @@ export class NumberField<Required extends true | false = false> extends Field<
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
override toType(): TFieldTSType {
|
||||
return {
|
||||
...super.toType(),
|
||||
type: "number",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { config } from "core";
|
||||
import type { Static } from "core/utils";
|
||||
import { Field, baseFieldConfigSchema } from "./Field";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
||||
const { Type } = tbbox;
|
||||
|
||||
export const primaryFieldConfigSchema = Type.Composite([
|
||||
@@ -48,4 +49,13 @@ export class PrimaryField<Required extends true | false = false> extends Field<
|
||||
override toJsonSchema() {
|
||||
return this.toSchemaWrapIfRequired(Type.Number({ writeOnly: undefined }));
|
||||
}
|
||||
|
||||
override toType(): TFieldTSType {
|
||||
return {
|
||||
...super.toType(),
|
||||
required: true,
|
||||
import: [{ package: "kysely", name: "Generated" }],
|
||||
type: "Generated<number>",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,4 +121,11 @@ export class TextField<Required extends true | false = false> extends Field<
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
override toType() {
|
||||
return {
|
||||
...super.toType(),
|
||||
type: "string",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import type { RelationType } from "./relation-types";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
const { Type } = tbbox;
|
||||
|
||||
const directions = ["source", "target"] as const;
|
||||
export type TDirection = (typeof directions)[number];
|
||||
|
||||
export type KyselyJsonFrom = any;
|
||||
export type KyselyQueryBuilder = SelectQueryBuilder<any, any, any>;
|
||||
|
||||
@@ -27,7 +30,7 @@ export abstract class EntityRelation<
|
||||
|
||||
// @todo: add unit tests
|
||||
// allowed directions, used in RelationAccessor for visibility
|
||||
directions: ("source" | "target")[] = ["source", "target"];
|
||||
directions: TDirection[] = ["source", "target"];
|
||||
|
||||
static schema = Type.Object({
|
||||
mappedBy: Type.Optional(Type.String()),
|
||||
@@ -102,6 +105,10 @@ export abstract class EntityRelation<
|
||||
);
|
||||
}
|
||||
|
||||
self(entity: Entity | string): EntityRelationAnchor {
|
||||
return this.other(entity).entity.name === this.source.entity.name ? this.target : this.source;
|
||||
}
|
||||
|
||||
ref(reference: string): EntityRelationAnchor {
|
||||
return this.source.reference === reference ? this.source : this.target;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Field, baseFieldConfigSchema } from "../fields";
|
||||
import type { EntityRelation } from "./EntityRelation";
|
||||
import type { EntityRelationAnchor } from "./EntityRelationAnchor";
|
||||
import * as tbbox from "@sinclair/typebox";
|
||||
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
||||
const { Type } = tbbox;
|
||||
|
||||
const CASCADES = ["cascade", "set null", "set default", "restrict", "no action"] as const;
|
||||
@@ -83,4 +84,11 @@ export class RelationField extends Field<RelationFieldConfig> {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
override toType(): TFieldTSType {
|
||||
return {
|
||||
...super.toType(),
|
||||
type: "number",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { $console, type PrimaryFieldType } from "core";
|
||||
import { $console, type AppEntity } from "core";
|
||||
import type { Entity, EntityManager } from "data";
|
||||
import { type FileUploadedEventData, Storage, type StorageAdapter, MediaPermissions } from "media";
|
||||
import { Module } from "modules/Module";
|
||||
@@ -17,8 +17,9 @@ import { buildMediaSchema, type mediaConfigSchema, registry } from "./media-sche
|
||||
|
||||
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
|
||||
declare module "core" {
|
||||
interface Media extends AppEntity, MediaFieldSchema {}
|
||||
interface DB {
|
||||
media: { id: PrimaryFieldType } & MediaFieldSchema;
|
||||
media: Media;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { DB, PrimaryFieldType } from "core";
|
||||
import { objectTransform } from "core/utils/objects";
|
||||
import { encodeSearch } from "core/utils/reqres";
|
||||
import type { EntityData, RepoQueryIn } from "data";
|
||||
import type { ModuleApi, ResponseObject } from "modules/ModuleApi";
|
||||
import useSWR, { type SWRConfiguration, mutate } from "swr";
|
||||
import type { EntityData, RepoQueryIn, RepositoryResponse } from "data";
|
||||
import type { Insertable, Selectable, Updateable } from "kysely";
|
||||
import type { FetchPromise, ModuleApi, ResponseObject } from "modules/ModuleApi";
|
||||
import useSWR, { type SWRConfiguration, type SWRResponse, mutate } from "swr";
|
||||
import { type Api, useApi } from "ui/client";
|
||||
|
||||
export class UseEntityApiError<Payload = any> extends Error {
|
||||
@@ -23,6 +24,26 @@ export class UseEntityApiError<Payload = any> extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
interface UseEntityReturn<
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined,
|
||||
Data = Entity extends keyof DB ? DB[Entity] : EntityData,
|
||||
Response = ResponseObject<RepositoryResponse<Selectable<Data>>>,
|
||||
> {
|
||||
create: (input: Insertable<Data>) => Promise<Response>;
|
||||
read: (
|
||||
query?: RepoQueryIn,
|
||||
) => Promise<
|
||||
ResponseObject<
|
||||
RepositoryResponse<Id extends undefined ? Selectable<Data>[] : Selectable<Data>>
|
||||
>
|
||||
>;
|
||||
update: Id extends undefined
|
||||
? (input: Updateable<Data>, id: Id) => Promise<Response>
|
||||
: (input: Updateable<Data>) => Promise<Response>;
|
||||
_delete: Id extends undefined ? (id: Id) => Promise<Response> : () => Promise<Response>;
|
||||
}
|
||||
|
||||
export const useEntity = <
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
@@ -30,28 +51,26 @@ export const useEntity = <
|
||||
>(
|
||||
entity: Entity,
|
||||
id?: Id,
|
||||
) => {
|
||||
): UseEntityReturn<Entity, Id, Data> => {
|
||||
const api = useApi().data;
|
||||
|
||||
return {
|
||||
create: async (input: Omit<Data, "id">) => {
|
||||
const res = await api.createOne(entity, input);
|
||||
create: async (input: Insertable<Data>) => {
|
||||
const res = await api.createOne(entity, input as any);
|
||||
if (!res.ok) {
|
||||
throw new UseEntityApiError(res, `Failed to create entity "${entity}"`);
|
||||
}
|
||||
return res;
|
||||
return res as any;
|
||||
},
|
||||
read: async (query: RepoQueryIn = {}) => {
|
||||
read: async (query?: RepoQueryIn) => {
|
||||
const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query);
|
||||
if (!res.ok) {
|
||||
throw new UseEntityApiError(res as any, `Failed to read entity "${entity}"`);
|
||||
}
|
||||
// must be manually typed
|
||||
return res as unknown as Id extends undefined
|
||||
? ResponseObject<Data[]>
|
||||
: ResponseObject<Data>;
|
||||
return res as any;
|
||||
},
|
||||
update: async (input: Partial<Omit<Data, "id">>, _id: PrimaryFieldType | undefined = id) => {
|
||||
// @ts-ignore
|
||||
update: async (input: Updateable<Data>, _id: PrimaryFieldType | undefined = id) => {
|
||||
if (!_id) {
|
||||
throw new Error("id is required");
|
||||
}
|
||||
@@ -59,8 +78,9 @@ export const useEntity = <
|
||||
if (!res.ok) {
|
||||
throw new UseEntityApiError(res, `Failed to update entity "${entity}"`);
|
||||
}
|
||||
return res;
|
||||
return res as any;
|
||||
},
|
||||
// @ts-ignore
|
||||
_delete: async (_id: PrimaryFieldType | undefined = id) => {
|
||||
if (!_id) {
|
||||
throw new Error("id is required");
|
||||
@@ -70,7 +90,7 @@ export const useEntity = <
|
||||
if (!res.ok) {
|
||||
throw new UseEntityApiError(res, `Failed to delete entity "${entity}"`);
|
||||
}
|
||||
return res;
|
||||
return res as any;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -91,6 +111,19 @@ export function makeKey(
|
||||
);
|
||||
}
|
||||
|
||||
interface UseEntityQueryReturn<
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
Data = Entity extends keyof DB ? Selectable<DB[Entity]> : EntityData,
|
||||
Return = Id extends undefined ? ResponseObject<Data[]> : ResponseObject<Data>,
|
||||
> extends Omit<SWRResponse<Return>, "mutate">,
|
||||
Omit<ReturnType<typeof useEntity<Entity, Id>>, "read"> {
|
||||
mutate: (id?: PrimaryFieldType) => Promise<any>;
|
||||
mutateRaw: SWRResponse<Return>["mutate"];
|
||||
api: Api["data"];
|
||||
key: string;
|
||||
}
|
||||
|
||||
export const useEntityQuery = <
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
@@ -99,11 +132,11 @@ export const useEntityQuery = <
|
||||
id?: Id,
|
||||
query?: RepoQueryIn,
|
||||
options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean },
|
||||
) => {
|
||||
): UseEntityQueryReturn<Entity, Id> => {
|
||||
const api = useApi().data;
|
||||
const key = makeKey(api, entity as string, id, query);
|
||||
const { read, ...actions } = useEntity<Entity, Id>(entity, id);
|
||||
const fetcher = () => read(query);
|
||||
const fetcher = () => read(query ?? {});
|
||||
|
||||
type T = Awaited<ReturnType<typeof fetcher>>;
|
||||
const swr = useSWR<T>(options?.enabled === false ? null : key, fetcher as any, {
|
||||
@@ -136,6 +169,7 @@ export const useEntityQuery = <
|
||||
...swr,
|
||||
...mapped,
|
||||
mutate: mutateFn,
|
||||
// @ts-ignore
|
||||
mutateRaw: swr.mutate,
|
||||
api,
|
||||
key,
|
||||
@@ -144,8 +178,8 @@ export const useEntityQuery = <
|
||||
|
||||
export async function mutateEntityCache<
|
||||
Entity extends keyof DB | string,
|
||||
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData,
|
||||
>(api: Api["data"], entity: Entity, id: PrimaryFieldType, partialData: Partial<Data>) {
|
||||
Data = Entity extends keyof DB ? DB[Entity] : EntityData,
|
||||
>(api: Api["data"], entity: Entity, id: PrimaryFieldType, partialData: Partial<Selectable<Data>>) {
|
||||
function update(prev: any, partialNext: any) {
|
||||
if (
|
||||
typeof prev !== "undefined" &&
|
||||
@@ -176,28 +210,37 @@ export async function mutateEntityCache<
|
||||
);
|
||||
}
|
||||
|
||||
interface UseEntityMutateReturn<
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
Data = Entity extends keyof DB ? DB[Entity] : EntityData,
|
||||
> extends Omit<ReturnType<typeof useEntityQuery<Entity, Id>>, "mutate"> {
|
||||
mutate: Id extends undefined
|
||||
? (id: PrimaryFieldType, data: Partial<Selectable<Data>>) => Promise<void>
|
||||
: (data: Partial<Selectable<Data>>) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useEntityMutate = <
|
||||
Entity extends keyof DB | string,
|
||||
Id extends PrimaryFieldType | undefined = undefined,
|
||||
Data = Entity extends keyof DB ? Omit<DB[Entity], "id"> : EntityData,
|
||||
Data = Entity extends keyof DB ? DB[Entity] : EntityData,
|
||||
>(
|
||||
entity: Entity,
|
||||
id?: Id,
|
||||
options?: SWRConfiguration,
|
||||
) => {
|
||||
): UseEntityMutateReturn<Entity, Id, Data> => {
|
||||
const { data, ...$q } = useEntityQuery<Entity, Id>(entity, id, undefined, {
|
||||
...options,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const _mutate = id
|
||||
? (data) => mutateEntityCache($q.api, entity, id, data)
|
||||
: (id, data) => mutateEntityCache($q.api, entity, id, data);
|
||||
? (data: Partial<Selectable<Data>>) => mutateEntityCache($q.api, entity, id, data)
|
||||
: (id: PrimaryFieldType, data: Partial<Selectable<Data>>) =>
|
||||
mutateEntityCache($q.api, entity, id, data);
|
||||
|
||||
return {
|
||||
...$q,
|
||||
mutate: _mutate as unknown as Id extends undefined
|
||||
? (id: PrimaryFieldType, data: Partial<Data>) => Promise<void>
|
||||
: (data: Partial<Data>) => Promise<void>,
|
||||
};
|
||||
mutate: _mutate,
|
||||
} as any;
|
||||
};
|
||||
|
||||
@@ -52,7 +52,6 @@ export function DataEntityUpdate({ params }) {
|
||||
}
|
||||
|
||||
async function onSubmitted(changeSet?: EntityData) {
|
||||
console.log("update:changeSet", changeSet);
|
||||
//return;
|
||||
if (!changeSet) {
|
||||
goBack();
|
||||
|
||||
@@ -45,8 +45,40 @@ function QueryMutateDataApi() {
|
||||
);
|
||||
}
|
||||
|
||||
function QueryMutateDataApi2() {
|
||||
const { mutate } = useEntityMutate("users");
|
||||
const { data, ...r } = useEntityQuery("users", undefined, {
|
||||
limit: 2,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
bla
|
||||
<pre>{JSON.stringify(r.key)}</pre>
|
||||
{r.error && <div>failed to load</div>}
|
||||
{r.isLoading && <div>loading...</div>}
|
||||
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
|
||||
{data && (
|
||||
<div>
|
||||
{data.map((user) => (
|
||||
<input
|
||||
key={String(user.id)}
|
||||
type="text"
|
||||
value={user.email}
|
||||
onChange={async (e) => {
|
||||
await mutate(user.id, { email: e.target.value });
|
||||
}}
|
||||
className="border border-black"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QueryDataApi() {
|
||||
const { data, update, ...r } = useEntityQuery("comments", undefined, {
|
||||
const { data, update, ...r } = useEntityQuery("users", undefined, {
|
||||
sort: { by: "id", dir: "asc" },
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
-1378
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ title: 'Introduction'
|
||||
description: 'Integrate bknd into your runtime/framework of choice'
|
||||
---
|
||||
|
||||
import { cloudflare, nextjs, reactRouter, astro, bun, node, docker, vite, aws } from "/snippets/integration-icons.mdx"
|
||||
import { cloudflare, nextjs, rr, astro, bun, node, docker, vite, aws } from "/snippets/integration-icons.mdx"
|
||||
|
||||
## Start with a Framework
|
||||
bknd seamlessly integrates with popular frameworks, allowing you to use what you're already familar with. The following guides will help you get started with your framework of choice.
|
||||
@@ -16,7 +16,7 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you
|
||||
/>
|
||||
<Card
|
||||
title="React Router"
|
||||
icon={<div className="text-primary-light">{reactRouter}</div>}
|
||||
icon={<div className="text-primary-light">{rr}</div>}
|
||||
href="/integration/react-router"
|
||||
/>
|
||||
<Card
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@
|
||||
"navigation": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": ["introduction", "motivation"]
|
||||
"pages": ["start", "motivation"]
|
||||
},
|
||||
{
|
||||
"group": "Usage",
|
||||
|
||||
Generated
+10855
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "bknd-docs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "mintlify dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mintlify": "^4.0.285"
|
||||
}
|
||||
}
|
||||
"name": "bknd-docs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "mintlify dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mintlify": "4.0.510"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ export const nextjs = <svg xmlns="http://www.w3.org/2000/svg" width="28px" heigh
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M9 15V9l7.745 10.65A9 9 0 1 1 19 17.657M15 12V9"/></svg>
|
||||
|
||||
export const reactRouter = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 256 140"><path d="M78.066 92.588c12.818 0 23.209-10.391 23.209-23.21c0-12.817-10.391-23.208-23.21-23.208c-12.817 0-23.208 10.39-23.208 23.209s10.391 23.209 23.209 23.209m-54.857 46.417c12.818 0 23.209-10.39 23.209-23.209c0-12.817-10.391-23.208-23.21-23.208C10.392 92.588 0 102.978 0 115.796s10.39 23.21 23.209 23.21m209.582 0c12.818 0 23.209-10.39 23.209-23.209c0-12.817-10.39-23.208-23.209-23.208s-23.209 10.39-23.209 23.208s10.391 23.21 23.21 23.21"/><path fill="currentColor" d="M156.565 70.357c-.742-7.754-1.12-14.208-7.06-18.744c-7.522-5.744-16.044-2.017-26.54-5.806C112.65 43.312 105 34.155 105 23.24C105 10.405 115.578 0 128.626 0c9.665 0 17.974 5.707 21.634 13.883c5.601 10.64 1.96 21.467 8.998 26.921c8.333 6.458 19.568 1.729 32.104 7.848a23.6 23.6 0 0 1 9.84 8.425A22.86 22.86 0 0 1 205 69.718c0 10.915-7.65 20.073-17.964 22.568c-10.497 3.789-19.019.062-26.541 5.806c-8.46 6.46-3.931 17.
|
||||
export const rr = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 256 140"><path d="M78.066 92.588c12.818 0 23.209-10.391 23.209-23.21c0-12.817-10.391-23.208-23.21-23.208c-12.817 0-23.208 10.39-23.208 23.209s10.391 23.209 23.209 23.209m-54.857 46.417c12.818 0 23.209-10.39 23.209-23.209c0-12.817-10.391-23.208-23.21-23.208C10.392 92.588 0 102.978 0 115.796s10.39 23.21 23.209 23.21m209.582 0c12.818 0 23.209-10.39 23.209-23.209c0-12.817-10.39-23.208-23.209-23.208s-23.209 10.39-23.209 23.208s10.391 23.21 23.21 23.21"/><path fill="currentColor" d="M156.565 70.357c-.742-7.754-1.12-14.208-7.06-18.744c-7.522-5.744-16.044-2.017-26.54-5.806C112.65 43.312 105 34.155 105 23.24C105 10.405 115.578 0 128.626 0c9.665 0 17.974 5.707 21.634 13.883c5.601 10.64 1.96 21.467 8.998 26.921c8.333 6.458 19.568 1.729 32.104 7.848a23.6 23.6 0 0 1 9.84 8.425A22.86 22.86 0 0 1 205 69.718c0 10.915-7.65 20.073-17.964 22.568c-10.497 3.789-19.019.062-26.541 5.806c-8.46 6.46-3.931 17.
|
||||
267-10.826 28.682c-3.913 7.518-11.867 12.663-21.043 12.663c-13.048 0-23.626-10.405-23.626-23.24c0-9.323 5.582-17.364 13.638-21.066c12.536-6.12 23.77-1.39 32.104-7.848c4.807-3.726 5.823-9.473 5.823-16.926"/></svg>;
|
||||
|
||||
export const astro = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
import { cloudflare, nextjs, reactRouter, astro, bun, node, docker, vite, aws, d1, libsql, sqlite, postgres, turso } from "/snippets/integration-icons.mdx"
|
||||
import { cloudflare, nextjs, rr, astro, bun, node, docker, vite, aws, d1, libsql, sqlite, postgres, turso } from "/snippets/integration-icons.mdx"
|
||||
import { Stackblitz, examples } from "/snippets/stackblitz.mdx"
|
||||
|
||||
Glad you're here! **bknd** is a lightweight, infrastructure agnostic and feature-rich backend that runs in any JavaScript environment.
|
||||
@@ -44,7 +44,7 @@ in the future, so stay tuned!
|
||||
/>
|
||||
<Card
|
||||
title="React Router"
|
||||
icon={<div className="text-primary-light">{reactRouter}</div>}
|
||||
icon={<div className="text-primary-light">{rr}</div>}
|
||||
href="/integration/react-router"
|
||||
/>
|
||||
<Card
|
||||
Reference in New Issue
Block a user