Compare commits

..

8 Commits

Author SHA1 Message Date
dswbx ee15150c78 Merge pull request #109 from bknd-io/release/0.10
Release 0.10
2025-03-25 13:04:25 +01:00
dswbx f8f5ef9c98 bump version to 0.10 2025-03-25 13:03:15 +01:00
dswbx ec015b7849 docs: add postgres and sqlocal instructions 2025-03-25 13:02:09 +01:00
dswbx 67e0374c04 add change set to mutator insert/update after event 2025-03-21 19:32:24 +01:00
dswbx 9380091ba9 Merge pull request #116 from bknd-io/fix/s3-upload-in-node
fix s3 media upload in node environments by adding content length to request
2025-03-21 18:05:04 +01:00
dswbx aa66a81b27 Merge pull request #114 from bknd-io/feat/admin-colors-and-resize
admin ui: started color centralization + made sidebar resizable
2025-03-21 18:04:26 +01:00
dswbx 0c15ec1434 hide oauth client details with type password 2025-03-21 18:03:45 +01:00
dswbx 7b8c7f1ae4 fix s3 media upload in node environments by adding content length to request 2025-03-21 18:01:44 +01:00
13 changed files with 137 additions and 142 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.10.0-rc.5",
"version": "0.10.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": {
+9 -2
View File
@@ -167,7 +167,9 @@ export class Mutator<
const res = await this.single(query);
await this.emgr.emit(new Mutator.Events.MutatorInsertAfter({ entity, data: res.data }));
await this.emgr.emit(
new Mutator.Events.MutatorInsertAfter({ entity, data: res.data, changed: validatedData }),
);
return res as any;
}
@@ -198,7 +200,12 @@ export class Mutator<
const res = await this.single(query);
await this.emgr.emit(
new Mutator.Events.MutatorUpdateAfter({ entity, entityId: id, data: res.data }),
new Mutator.Events.MutatorUpdateAfter({
entity,
entityId: id,
data: res.data,
changed: validatedData,
}),
);
return res as any;
+6 -1
View File
@@ -18,7 +18,11 @@ export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityDat
});
}
}
export class MutatorInsertAfter extends Event<{ entity: Entity; data: EntityData }> {
export class MutatorInsertAfter extends Event<{
entity: Entity;
data: EntityData;
changed: EntityData;
}> {
static override slug = "mutator-insert-after";
}
export class MutatorUpdateBefore extends Event<
@@ -48,6 +52,7 @@ export class MutatorUpdateAfter extends Event<{
entity: Entity;
entityId: PrimaryFieldType;
data: EntityData;
changed: EntityData;
}> {
static override slug = "mutator-update-after";
}
@@ -118,14 +118,20 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
const res = await this.fetch(url, {
method: "PUT",
body,
headers: isFile(body)
? {
// required for node environments
"Content-Length": String(body.size),
}
: {},
});
if (res.ok) {
// "df20fcb574dba1446cf5ec997940492b"
return String(res.headers.get("etag"));
if (!res.ok) {
throw new Error(`Failed to upload object: ${res.status} ${res.statusText}`);
}
return undefined;
// "df20fcb574dba1446cf5ec997940492b"
return String(res.headers.get("etag"));
}
private async headObject(
+12 -14
View File
@@ -22,20 +22,17 @@ type CanvasProps = ReactFlowProps & {
onDropNewEdge?: (base: any) => any;
};
export function Canvas(
{
nodes: _nodes,
edges: _edges,
externalProvider,
backgroundStyle = "lines",
minimap = false,
children,
onDropNewNode,
onDropNewEdge,
...props
}: CanvasProps,
ref?: any,
) {
export function Canvas({
nodes: _nodes,
edges: _edges,
externalProvider,
backgroundStyle = "lines",
minimap = false,
children,
onDropNewNode,
onDropNewEdge,
...props
}: CanvasProps) {
const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []);
const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []);
const { screenToFlowPosition } = useReactFlow();
@@ -179,6 +176,7 @@ export function Canvas(
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodesConnectable={false}
/*panOnDrag={isSpacePressed}*/
panOnDrag={true}
zoomOnScroll={isCommandPressed}
panOnScroll={!isCommandPressed}
@@ -44,10 +44,8 @@ export const layoutWithDagre = ({ nodes, edges, graph }: LayoutProps) => {
const position = dagreGraph.node(node.id);
return {
...node,
position: {
x: position.x - (node.width ?? 0) / 2,
y: position.y - (node.height ?? 0) / 2,
},
x: position.x - (node.width ?? 0) / 2,
y: position.y - (node.height ?? 0) / 2,
};
}),
edges,
+10 -8
View File
@@ -35,14 +35,16 @@ export function Panels({ children, ...props }: PanelsProps) {
)}
<Panel unstyled position="bottom-right">
{props.zoom && (
<Panel.Wrapper className="px-1.5">
<Panel.IconButton Icon={TbPlus} round onClick={handleZoomIn} />
<Panel.Text className="px-2" mono onClick={handleZoomReset}>
{percent}%
</Panel.Text>
<Panel.IconButton Icon={TbMinus} round onClick={handleZoomOut} />
<Panel.IconButton Icon={TbMaximize} round onClick={handleZoomReset} />
</Panel.Wrapper>
<>
<Panel.Wrapper className="px-1.5">
<Panel.IconButton Icon={TbPlus} round onClick={handleZoomIn} />
<Panel.Text className="px-2" mono onClick={handleZoomReset}>
{percent}%
</Panel.Text>
<Panel.IconButton Icon={TbMinus} round onClick={handleZoomOut} />
<Panel.IconButton Icon={TbMaximize} round onClick={handleZoomReset} />
</Panel.Wrapper>
</>
)}
{props.minimap && (
<>
+7 -10
View File
@@ -4,15 +4,12 @@
// there is no lifecycle or Hook in React that we can use to switch
// .current at the right timing."
// So we will have to make do with this "close enough" approach for now.
import { useEffect, useRef } from "react";
import { useLayoutEffect, useRef } from "react";
import { isDebug } from "core";
export const useEvent = <Fn>(fn: Fn | ((...args: any[]) => any) | undefined): Fn => {
const ref = useRef([fn, (...args) => ref[0](...args)]).current;
// Per Dan Abramov: useInsertionEffect executes marginally closer to the
// correct timing for ref synchronization than useLayoutEffect on React 18.
// See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360
useEffect(() => {
ref[0] = fn;
}, []);
return ref[1];
export const useEvent = <Fn>(fn: Fn): Fn => {
if (isDebug()) {
console.warn("useEvent() is deprecated");
}
return fn;
};
@@ -6,10 +6,6 @@ import { layoutWithDagre } from "ui/components/canvas/layouts";
import { Panels } from "ui/components/canvas/panels";
import { EntityTableNode } from "./EntityTableNode";
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>[] {
return Object.entries(entities ?? {}).map(([name, entity]) => {
@@ -69,45 +65,12 @@ const nodeTypes = {
entity: EntityTableNode.Component,
} 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() {
const {
config: { data },
} = useBknd();
const { theme } = useTheme();
const nodes = entitiesToNodes(data.entities);
const edges = relationsToEdges(data.relations).map((e) => ({
...e,
style: {
@@ -122,35 +85,31 @@ export function DataSchemaCanvas() {
},
}));
const entityNodes = entitiesToNodes(data.entities);
const positions = dataCanvasStore((state) => state.positions);
const setPositions = dataCanvasStore((state) => state.setPositions);
const resetPositions = dataCanvasStore((state) => state.reset);
const nodeLayout = layoutWithDagre({
nodes: nodes.map((n) => ({
id: n.id,
...EntityTableNode.getSize(n.data),
})),
edges,
graph: {
rankdir: "LR",
marginx: 50,
marginy: 50,
},
});
const layout = positions ? positions : getNodeAutoLayout(entityNodes, edges);
const [nodes, setNodes] = useState<Node<TAppDataEntity>[]>(setNodesLayout(entityNodes, layout));
function setLayout(positions: CanvasPosition[] = getNodeAutoLayout(entityNodes, edges)) {
setNodes(setNodesLayout(entityNodes, positions));
}
function resetLayout() {
resetPositions();
setLayout();
}
nodeLayout.nodes.forEach((node) => {
const n = nodes.find((n) => n.id === node.id);
if (n) {
n.position = { x: node.x, y: node.y };
}
});
return (
<ReactFlowProvider>
<Canvas
nodes={nodes}
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}
minZoom={0.1}
maxZoom={2}
@@ -159,11 +118,7 @@ export function DataSchemaCanvas() {
maxZoom: 0.8,
}}
>
<Panels zoom minimap>
<Panel position="bottom-left">
<Panel.IconButton round Icon={TbLayout} onClick={resetLayout} />
</Panel>
</Panels>
<Panels zoom minimap />
</Canvas>
</ReactFlowProvider>
);
+2 -2
View File
@@ -240,8 +240,8 @@ const StrategyPasswordForm = () => {
const StrategyOAuthForm = () => {
return (
<>
<Field name="config.client.client_id" required />
<Field name="config.client.client_secret" required />
<Field name="config.client.client_id" required inputProps={{ type: "password" }} />
<Field name="config.client.client_secret" required inputProps={{ type: "password" }} />
</>
);
};
-25
View File
@@ -1,25 +0,0 @@
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
View File
@@ -1,2 +1 @@
export { appShellStore } from "./appshell";
export { dataCanvasStore, type CanvasPosition } from "./datacanvas";
+58 -5
View File
@@ -55,12 +55,65 @@ connection object to your new database:
}
```
### 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>
### 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
Any bknd app instantiation accepts as connection either `undefined`, a connection object like
described above, or an class instance that extends from `Connection`: