mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-04 00:56:01 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3180037b67 |
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.10.0",
|
"version": "0.10.0-rc.5",
|
||||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
"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",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -167,9 +167,7 @@ export class Mutator<
|
|||||||
|
|
||||||
const res = await this.single(query);
|
const res = await this.single(query);
|
||||||
|
|
||||||
await this.emgr.emit(
|
await this.emgr.emit(new Mutator.Events.MutatorInsertAfter({ entity, data: res.data }));
|
||||||
new Mutator.Events.MutatorInsertAfter({ entity, data: res.data, changed: validatedData }),
|
|
||||||
);
|
|
||||||
|
|
||||||
return res as any;
|
return res as any;
|
||||||
}
|
}
|
||||||
@@ -200,12 +198,7 @@ export class Mutator<
|
|||||||
const res = await this.single(query);
|
const res = await this.single(query);
|
||||||
|
|
||||||
await this.emgr.emit(
|
await this.emgr.emit(
|
||||||
new Mutator.Events.MutatorUpdateAfter({
|
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data }),
|
||||||
entity,
|
|
||||||
entityId: id,
|
|
||||||
data: res.data,
|
|
||||||
changed: validatedData,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return res as any;
|
return res as any;
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityDat
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class MutatorInsertAfter extends Event<{
|
export class MutatorInsertAfter extends Event<{ entity: Entity; data: EntityData }> {
|
||||||
entity: Entity;
|
|
||||||
data: EntityData;
|
|
||||||
changed: EntityData;
|
|
||||||
}> {
|
|
||||||
static override slug = "mutator-insert-after";
|
static override slug = "mutator-insert-after";
|
||||||
}
|
}
|
||||||
export class MutatorUpdateBefore extends Event<
|
export class MutatorUpdateBefore extends Event<
|
||||||
@@ -52,7 +48,6 @@ export class MutatorUpdateAfter extends Event<{
|
|||||||
entity: Entity;
|
entity: Entity;
|
||||||
entityId: PrimaryFieldType;
|
entityId: PrimaryFieldType;
|
||||||
data: EntityData;
|
data: EntityData;
|
||||||
changed: EntityData;
|
|
||||||
}> {
|
}> {
|
||||||
static override slug = "mutator-update-after";
|
static override slug = "mutator-update-after";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,20 +118,14 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
|
|||||||
const res = await this.fetch(url, {
|
const res = await this.fetch(url, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body,
|
body,
|
||||||
headers: isFile(body)
|
|
||||||
? {
|
|
||||||
// required for node environments
|
|
||||||
"Content-Length": String(body.size),
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (res.ok) {
|
||||||
throw new Error(`Failed to upload object: ${res.status} ${res.statusText}`);
|
// "df20fcb574dba1446cf5ec997940492b"
|
||||||
|
return String(res.headers.get("etag"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// "df20fcb574dba1446cf5ec997940492b"
|
return undefined;
|
||||||
return String(res.headers.get("etag"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async headObject(
|
private async headObject(
|
||||||
|
|||||||
@@ -22,17 +22,20 @@ type CanvasProps = ReactFlowProps & {
|
|||||||
onDropNewEdge?: (base: any) => any;
|
onDropNewEdge?: (base: any) => any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Canvas({
|
export function Canvas(
|
||||||
nodes: _nodes,
|
{
|
||||||
edges: _edges,
|
nodes: _nodes,
|
||||||
externalProvider,
|
edges: _edges,
|
||||||
backgroundStyle = "lines",
|
externalProvider,
|
||||||
minimap = false,
|
backgroundStyle = "lines",
|
||||||
children,
|
minimap = false,
|
||||||
onDropNewNode,
|
children,
|
||||||
onDropNewEdge,
|
onDropNewNode,
|
||||||
...props
|
onDropNewEdge,
|
||||||
}: CanvasProps) {
|
...props
|
||||||
|
}: CanvasProps,
|
||||||
|
ref?: any,
|
||||||
|
) {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []);
|
const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []);
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
@@ -176,7 +179,6 @@ export function Canvas({
|
|||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
/*panOnDrag={isSpacePressed}*/
|
|
||||||
panOnDrag={true}
|
panOnDrag={true}
|
||||||
zoomOnScroll={isCommandPressed}
|
zoomOnScroll={isCommandPressed}
|
||||||
panOnScroll={!isCommandPressed}
|
panOnScroll={!isCommandPressed}
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ export const layoutWithDagre = ({ nodes, edges, graph }: LayoutProps) => {
|
|||||||
const position = dagreGraph.node(node.id);
|
const position = dagreGraph.node(node.id);
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
x: position.x - (node.width ?? 0) / 2,
|
position: {
|
||||||
y: position.y - (node.height ?? 0) / 2,
|
x: position.x - (node.width ?? 0) / 2,
|
||||||
|
y: position.y - (node.height ?? 0) / 2,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
edges,
|
edges,
|
||||||
|
|||||||
@@ -35,16 +35,14 @@ export function Panels({ children, ...props }: PanelsProps) {
|
|||||||
)}
|
)}
|
||||||
<Panel unstyled position="bottom-right">
|
<Panel unstyled position="bottom-right">
|
||||||
{props.zoom && (
|
{props.zoom && (
|
||||||
<>
|
<Panel.Wrapper className="px-1.5">
|
||||||
<Panel.Wrapper className="px-1.5">
|
<Panel.IconButton Icon={TbPlus} round onClick={handleZoomIn} />
|
||||||
<Panel.IconButton Icon={TbPlus} round onClick={handleZoomIn} />
|
<Panel.Text className="px-2" mono onClick={handleZoomReset}>
|
||||||
<Panel.Text className="px-2" mono onClick={handleZoomReset}>
|
{percent}%
|
||||||
{percent}%
|
</Panel.Text>
|
||||||
</Panel.Text>
|
<Panel.IconButton Icon={TbMinus} round onClick={handleZoomOut} />
|
||||||
<Panel.IconButton Icon={TbMinus} round onClick={handleZoomOut} />
|
<Panel.IconButton Icon={TbMaximize} round onClick={handleZoomReset} />
|
||||||
<Panel.IconButton Icon={TbMaximize} round onClick={handleZoomReset} />
|
</Panel.Wrapper>
|
||||||
</Panel.Wrapper>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{props.minimap && (
|
{props.minimap && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
// there is no lifecycle or Hook in React that we can use to switch
|
// there is no lifecycle or Hook in React that we can use to switch
|
||||||
// .current at the right timing."
|
// .current at the right timing."
|
||||||
// So we will have to make do with this "close enough" approach for now.
|
// So we will have to make do with this "close enough" approach for now.
|
||||||
import { useLayoutEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { isDebug } from "core";
|
|
||||||
|
|
||||||
export const useEvent = <Fn>(fn: Fn): Fn => {
|
export const useEvent = <Fn>(fn: Fn | ((...args: any[]) => any) | undefined): Fn => {
|
||||||
if (isDebug()) {
|
const ref = useRef([fn, (...args) => ref[0](...args)]).current;
|
||||||
console.warn("useEvent() is deprecated");
|
// Per Dan Abramov: useInsertionEffect executes marginally closer to the
|
||||||
}
|
// correct timing for ref synchronization than useLayoutEffect on React 18.
|
||||||
return fn;
|
// See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360
|
||||||
|
useEffect(() => {
|
||||||
|
ref[0] = fn;
|
||||||
|
}, []);
|
||||||
|
return ref[1];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { layoutWithDagre } from "ui/components/canvas/layouts";
|
|||||||
import { Panels } from "ui/components/canvas/panels";
|
import { Panels } from "ui/components/canvas/panels";
|
||||||
import { EntityTableNode } from "./EntityTableNode";
|
import { EntityTableNode } from "./EntityTableNode";
|
||||||
import { useTheme } from "ui/client/use-theme";
|
import { useTheme } from "ui/client/use-theme";
|
||||||
|
import { Panel } from "ui/components/canvas/panels/Panel";
|
||||||
|
import { TbLayout } from "react-icons/tb";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { type CanvasPosition, dataCanvasStore } from "ui/store";
|
||||||
|
|
||||||
function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEntity>[] {
|
function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEntity>[] {
|
||||||
return Object.entries(entities ?? {}).map(([name, entity]) => {
|
return Object.entries(entities ?? {}).map(([name, entity]) => {
|
||||||
@@ -65,12 +69,45 @@ const nodeTypes = {
|
|||||||
entity: EntityTableNode.Component,
|
entity: EntityTableNode.Component,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
function getNodeAutoLayout(nodes: Node<TAppDataEntity>[], edges: any[]): CanvasPosition[] {
|
||||||
|
const nodeLayout = layoutWithDagre({
|
||||||
|
nodes: nodes.map((n) => ({
|
||||||
|
id: n.id,
|
||||||
|
...EntityTableNode.getSize(n.data),
|
||||||
|
})),
|
||||||
|
edges,
|
||||||
|
graph: {
|
||||||
|
rankdir: "LR",
|
||||||
|
marginx: 50,
|
||||||
|
marginy: 50,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodeLayout.nodes.map((n) => ({
|
||||||
|
id: n.id,
|
||||||
|
...n.position,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNodesLayout(nodes: Node<TAppDataEntity>[], layout: CanvasPosition[]) {
|
||||||
|
return nodes.map((node) => {
|
||||||
|
const pos = layout.find((l) => l.id === node.id);
|
||||||
|
if (pos) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
position: { x: pos.x, y: pos.y },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function DataSchemaCanvas() {
|
export function DataSchemaCanvas() {
|
||||||
const {
|
const {
|
||||||
config: { data },
|
config: { data },
|
||||||
} = useBknd();
|
} = useBknd();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const nodes = entitiesToNodes(data.entities);
|
|
||||||
const edges = relationsToEdges(data.relations).map((e) => ({
|
const edges = relationsToEdges(data.relations).map((e) => ({
|
||||||
...e,
|
...e,
|
||||||
style: {
|
style: {
|
||||||
@@ -85,31 +122,35 @@ export function DataSchemaCanvas() {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const nodeLayout = layoutWithDagre({
|
const entityNodes = entitiesToNodes(data.entities);
|
||||||
nodes: nodes.map((n) => ({
|
const positions = dataCanvasStore((state) => state.positions);
|
||||||
id: n.id,
|
const setPositions = dataCanvasStore((state) => state.setPositions);
|
||||||
...EntityTableNode.getSize(n.data),
|
const resetPositions = dataCanvasStore((state) => state.reset);
|
||||||
})),
|
|
||||||
edges,
|
|
||||||
graph: {
|
|
||||||
rankdir: "LR",
|
|
||||||
marginx: 50,
|
|
||||||
marginy: 50,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
nodeLayout.nodes.forEach((node) => {
|
const layout = positions ? positions : getNodeAutoLayout(entityNodes, edges);
|
||||||
const n = nodes.find((n) => n.id === node.id);
|
const [nodes, setNodes] = useState<Node<TAppDataEntity>[]>(setNodesLayout(entityNodes, layout));
|
||||||
if (n) {
|
|
||||||
n.position = { x: node.x, y: node.y };
|
function setLayout(positions: CanvasPosition[] = getNodeAutoLayout(entityNodes, edges)) {
|
||||||
}
|
setNodes(setNodesLayout(entityNodes, positions));
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function resetLayout() {
|
||||||
|
resetPositions();
|
||||||
|
setLayout();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<Canvas
|
<Canvas
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
|
onNodeDragStop={(e, node) => {
|
||||||
|
const positions = nodes
|
||||||
|
.map((n) => (n.id === node.id ? node : n))
|
||||||
|
.map((n) => ({ id: n.id, ...n.position }));
|
||||||
|
setPositions(positions);
|
||||||
|
setLayout(positions);
|
||||||
|
}}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
minZoom={0.1}
|
minZoom={0.1}
|
||||||
maxZoom={2}
|
maxZoom={2}
|
||||||
@@ -118,7 +159,11 @@ export function DataSchemaCanvas() {
|
|||||||
maxZoom: 0.8,
|
maxZoom: 0.8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Panels zoom minimap />
|
<Panels zoom minimap>
|
||||||
|
<Panel position="bottom-left">
|
||||||
|
<Panel.IconButton round Icon={TbLayout} onClick={resetLayout} />
|
||||||
|
</Panel>
|
||||||
|
</Panels>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -240,8 +240,8 @@ const StrategyPasswordForm = () => {
|
|||||||
const StrategyOAuthForm = () => {
|
const StrategyOAuthForm = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Field name="config.client.client_id" required inputProps={{ type: "password" }} />
|
<Field name="config.client.client_id" required />
|
||||||
<Field name="config.client.client_secret" required inputProps={{ type: "password" }} />
|
<Field name="config.client.client_secret" required />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { combine, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export type CanvasPosition = {
|
||||||
|
id: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dataCanvasStore = create(
|
||||||
|
persist(
|
||||||
|
combine(
|
||||||
|
{
|
||||||
|
positions: null as CanvasPosition[] | null,
|
||||||
|
},
|
||||||
|
(set) => ({
|
||||||
|
setPositions: (positions: CanvasPosition[]) => set(() => ({ positions })),
|
||||||
|
reset: () => set(() => ({ positions: null })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
name: "datacanvas",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { appShellStore } from "./appshell";
|
export { appShellStore } from "./appshell";
|
||||||
|
export { dataCanvasStore, type CanvasPosition } from "./datacanvas";
|
||||||
|
|||||||
+5
-58
@@ -55,65 +55,12 @@ connection object to your new database:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cloudflare D1
|
|
||||||
Using the [Cloudflare Adapter](/integration/cloudflare), you can choose to use a D1 database binding. To do so, you only need to add a D1 database to your `wrangler.toml` and it'll pick up automatically. To manually specify which D1 database to take, you can specify it manually:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { serve, d1 } from "bknd/adapter/cloudflare";
|
|
||||||
|
|
||||||
export default serve<Env>({
|
|
||||||
app: ({ env }) => d1({ binding: env.D1_BINDING })
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### PostgreSQL
|
|
||||||
To use bknd with Postgres, you need to install the `@bknd/postgres` package. You can do so by running the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @bknd/postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
This package uses `pg` under the hood. If you'd like to see `postgres` or any other flavor, please create an [issue on Github](https://github.com/bknd-io/bknd/issues/new).
|
|
||||||
|
|
||||||
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package. Here is a quick example using the [Node.js Adapter](http://localhost:3000/integration/node):
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { serve } from "bknd/adapter/node";
|
|
||||||
import { PostgresConnection } from "@bknd/postgres";
|
|
||||||
|
|
||||||
/** @type {import("bknd/adapter/node").NodeBkndConfig} */
|
|
||||||
const config = {
|
|
||||||
connection: new PostgresConnection({
|
|
||||||
connectionString:
|
|
||||||
"postgresql://user:password@localhost:5432/database",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
serve(config);
|
|
||||||
```
|
|
||||||
|
|
||||||
### SQLocal
|
|
||||||
To use bknd with `sqlocal` for a offline expierence, you need to install the `@bknd/sqlocal` package. You can do so by running the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @bknd/sqlocal
|
|
||||||
```
|
|
||||||
|
|
||||||
This package uses `sqlocal` under the hood. Consult the [sqlocal documentation](https://sqlocal.dallashoffman.com/guide/setup) for connection options:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { createApp } from "bknd";
|
|
||||||
import { SQLocalConnection } from "@bknd/sqlocal";
|
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
connection: new SQLocalConnection({
|
|
||||||
databasePath: ":localStorage:",
|
|
||||||
verbose: true,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Connection
|
### Custom Connection
|
||||||
|
<Note>
|
||||||
|
Follow the progress of custom connections on its [Github Issue](https://github.com/bknd-io/bknd/issues/24).
|
||||||
|
If you're interested, make sure to upvote so it can be prioritized.
|
||||||
|
</Note>
|
||||||
|
|
||||||
Any bknd app instantiation accepts as connection either `undefined`, a connection object like
|
Any bknd app instantiation accepts as connection either `undefined`, a connection object like
|
||||||
described above, or an class instance that extends from `Connection`:
|
described above, or an class instance that extends from `Connection`:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user