mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
adding d1 for cloudflare environments
This commit is contained in:
262
app/build.ts
262
app/build.ts
@@ -49,111 +49,117 @@ if (types && !watch) {
|
||||
/**
|
||||
* Building backend and general API
|
||||
*/
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
||||
outDir: "dist",
|
||||
external: ["bun:test", "@libsql/client"],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
});
|
||||
async function buildApi() {
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
||||
outDir: "dist",
|
||||
external: ["bun:test", "@libsql/client"],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Building UI for direct imports
|
||||
*/
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
outDir: "dist/ui",
|
||||
external: [
|
||||
"bun:test",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store",
|
||||
/codemirror/,
|
||||
"@xyflow/react",
|
||||
"@mantine/core"
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.logLevel = "silent";
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
});
|
||||
async function buildUi() {
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||
outDir: "dist/ui",
|
||||
external: [
|
||||
"bun:test",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store",
|
||||
/codemirror/,
|
||||
"@xyflow/react",
|
||||
"@mantine/core"
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.logLevel = "silent";
|
||||
},
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Building UI Elements
|
||||
* - tailwind-merge is mocked, no exclude
|
||||
* - ui/client is external, and after built replaced with "bknd/client"
|
||||
*/
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/ui/elements/index.ts"],
|
||||
outDir: "dist/ui/elements",
|
||||
external: [
|
||||
"ui/client",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store"
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.alias = {
|
||||
// not important for elements, mock to reduce bundle
|
||||
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts"
|
||||
};
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// manually replace ui/client with bknd/client
|
||||
const path = "./dist/ui/elements/index.js";
|
||||
const bundle = await Bun.file(path).text();
|
||||
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
|
||||
async function buildUiElements() {
|
||||
await tsup.build({
|
||||
minify,
|
||||
sourcemap,
|
||||
watch,
|
||||
entry: ["src/ui/elements/index.ts"],
|
||||
outDir: "dist/ui/elements",
|
||||
external: [
|
||||
"ui/client",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"use-sync-external-store"
|
||||
],
|
||||
metafile: true,
|
||||
platform: "browser",
|
||||
format: ["esm"],
|
||||
splitting: false,
|
||||
bundle: true,
|
||||
treeshake: true,
|
||||
loader: {
|
||||
".svg": "dataurl"
|
||||
},
|
||||
esbuildOptions: (options) => {
|
||||
options.alias = {
|
||||
// not important for elements, mock to reduce bundle
|
||||
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts"
|
||||
};
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// manually replace ui/client with bknd/client
|
||||
const path = "./dist/ui/elements/index.js";
|
||||
const bundle = await Bun.file(path).text();
|
||||
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
|
||||
|
||||
delayTypes();
|
||||
}
|
||||
});
|
||||
delayTypes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Building adapters
|
||||
*/
|
||||
function baseConfig(adapter: string): tsup.Options {
|
||||
function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsup.Options {
|
||||
return {
|
||||
minify,
|
||||
sourcemap,
|
||||
@@ -162,47 +168,61 @@ function baseConfig(adapter: string): tsup.Options {
|
||||
format: ["esm"],
|
||||
platform: "neutral",
|
||||
outDir: `dist/adapter/${adapter}`,
|
||||
metafile: true,
|
||||
splitting: false,
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
},
|
||||
...overrides,
|
||||
define: {
|
||||
__isDev: "0"
|
||||
__isDev: "0",
|
||||
...overrides.define
|
||||
},
|
||||
external: [
|
||||
/^cloudflare*/,
|
||||
/^@?(hono|libsql).*?/,
|
||||
/^(bknd|react|next|node).*?/,
|
||||
/.*\.(html)$/
|
||||
],
|
||||
metafile: true,
|
||||
splitting: false,
|
||||
onSuccess: async () => {
|
||||
delayTypes();
|
||||
}
|
||||
/.*\.(html)$/,
|
||||
...(Array.isArray(overrides.external) ? overrides.external : [])
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// base adapter handles
|
||||
await tsup.build({
|
||||
...baseConfig(""),
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter"
|
||||
});
|
||||
async function buildAdapters() {
|
||||
// base adapter handles
|
||||
await tsup.build({
|
||||
...baseConfig(""),
|
||||
entry: ["src/adapter/index.ts"],
|
||||
outDir: "dist/adapter"
|
||||
});
|
||||
|
||||
// specific adatpers
|
||||
await tsup.build(baseConfig("remix"));
|
||||
await tsup.build(baseConfig("bun"));
|
||||
await tsup.build(baseConfig("astro"));
|
||||
await tsup.build(baseConfig("cloudflare"));
|
||||
// specific adatpers
|
||||
await tsup.build(baseConfig("remix"));
|
||||
await tsup.build(baseConfig("bun"));
|
||||
await tsup.build(baseConfig("astro"));
|
||||
await tsup.build(
|
||||
baseConfig("cloudflare", {
|
||||
external: [/^kysely/]
|
||||
})
|
||||
);
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node"
|
||||
});
|
||||
await tsup.build({
|
||||
...baseConfig("vite"),
|
||||
platform: "node"
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node"
|
||||
});
|
||||
await tsup.build({
|
||||
...baseConfig("nextjs"),
|
||||
platform: "node"
|
||||
});
|
||||
|
||||
await tsup.build({
|
||||
...baseConfig("node"),
|
||||
platform: "node"
|
||||
});
|
||||
await tsup.build({
|
||||
...baseConfig("node"),
|
||||
platform: "node"
|
||||
});
|
||||
}
|
||||
|
||||
await buildApi();
|
||||
await buildUi();
|
||||
await buildUiElements();
|
||||
await buildAdapters();
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"esbuild-postcss": "^0.0.4",
|
||||
"jotai": "^2.10.1",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"open": "^10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"postcss": "^8.4.47",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { FrameworkBkndConfig } from "bknd/adapter";
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { type FrameworkBkndConfig, makeConfig } from "bknd/adapter";
|
||||
import { Hono } from "hono";
|
||||
import { serveStatic } from "hono/cloudflare-workers";
|
||||
import { D1Connection } from "./connection/D1Connection";
|
||||
import { getCached } from "./modes/cached";
|
||||
import { getDurable } from "./modes/durable";
|
||||
import { getFresh, getWarm } from "./modes/fresh";
|
||||
@@ -10,6 +13,7 @@ export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>>
|
||||
bindings?: (args: Context<Env>) => {
|
||||
kv?: KVNamespace;
|
||||
dobj?: DurableObjectNamespace;
|
||||
db?: D1Database;
|
||||
};
|
||||
static?: "kv" | "assets";
|
||||
key?: string;
|
||||
@@ -26,7 +30,7 @@ export type Context<Env = any> = {
|
||||
ctx: ExecutionContext;
|
||||
};
|
||||
|
||||
export function serve<Env = any>(config: CloudflareBkndConfig<Env>) {
|
||||
export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
||||
return {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||
const url = new URL(request.url);
|
||||
@@ -61,20 +65,46 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env>) {
|
||||
}
|
||||
}
|
||||
|
||||
config.setAdminHtml = config.setAdminHtml && !!config.manifest;
|
||||
|
||||
const context = { request, env, ctx } as Context;
|
||||
const mode = config.mode ?? "warm";
|
||||
|
||||
const appConfig = makeConfig(config, context);
|
||||
const bindings = config.bindings?.(context);
|
||||
if (!appConfig.connection) {
|
||||
let db: D1Database | undefined;
|
||||
if (bindings && "db" in bindings && bindings.db) {
|
||||
console.log("Using database from bindings");
|
||||
db = bindings.db;
|
||||
} else if (env && Object.keys(env).length > 0) {
|
||||
// try to find a database in env
|
||||
for (const key in env) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
if (env[key].constructor.name === "D1Database") {
|
||||
console.log(`Using database from env "${key}"`);
|
||||
db = env[key] as D1Database;
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (db) {
|
||||
appConfig.connection = new D1Connection({ binding: db });
|
||||
} else {
|
||||
throw new Error("No database connection given");
|
||||
}
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case "fresh":
|
||||
return await getFresh(config, context);
|
||||
return await getFresh(appConfig, context);
|
||||
case "warm":
|
||||
return await getWarm(config, context);
|
||||
return await getWarm(appConfig, context);
|
||||
case "cache":
|
||||
return await getCached(config, context);
|
||||
return await getCached(appConfig, context);
|
||||
case "durable":
|
||||
return await getDurable(config, context);
|
||||
return await getDurable(appConfig, context);
|
||||
default:
|
||||
throw new Error(`Unknown mode ${mode}`);
|
||||
}
|
||||
|
||||
65
app/src/adapter/cloudflare/connection/D1Connection.ts
Normal file
65
app/src/adapter/cloudflare/connection/D1Connection.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { SqliteConnection } from "bknd/data";
|
||||
import { KyselyPluginRunner } from "data";
|
||||
import type { QB } from "data/connection/Connection";
|
||||
import { SqliteIntrospector } from "data/connection/SqliteIntrospector";
|
||||
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
|
||||
import { D1Dialect } from "kysely-d1";
|
||||
|
||||
export type D1ConnectionConfig = {
|
||||
binding: D1Database;
|
||||
};
|
||||
|
||||
class CustomD1Dialect extends D1Dialect {
|
||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db, {
|
||||
excludeTables: [""]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class D1Connection extends SqliteConnection {
|
||||
constructor(private config: D1ConnectionConfig) {
|
||||
const plugins = [new ParseJSONResultsPlugin()];
|
||||
|
||||
const kysely = new Kysely({
|
||||
dialect: new CustomD1Dialect({ database: config.binding }),
|
||||
plugins
|
||||
});
|
||||
super(kysely, {}, plugins);
|
||||
}
|
||||
|
||||
override supportsBatching(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override supportsIndices(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override async batch<Queries extends QB[]>(
|
||||
queries: [...Queries]
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
const db = this.config.binding;
|
||||
|
||||
const res = await db.batch(
|
||||
queries.map((q) => {
|
||||
const { sql, parameters } = q.compile();
|
||||
return db.prepare(sql).bind(...parameters);
|
||||
})
|
||||
);
|
||||
|
||||
// let it run through plugins
|
||||
const kyselyPlugins = new KyselyPluginRunner(this.plugins);
|
||||
const data: any = [];
|
||||
for (const r of res) {
|
||||
const rows = await kyselyPlugins.transformResultRows(r.results);
|
||||
data.push(rows);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { D1Connection, type D1ConnectionConfig } from "./connection/D1Connection";
|
||||
|
||||
export * from "./cloudflare-workers.adapter";
|
||||
export { makeApp, getFresh, getWarm } from "./modes/fresh";
|
||||
export { getCached } from "./modes/cached";
|
||||
export { DurableBkndApp, getDurable } from "./modes/durable";
|
||||
export { D1Connection, type D1ConnectionConfig };
|
||||
|
||||
export function d1(config: D1ConnectionConfig) {
|
||||
return new D1Connection(config);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ export class LibsqlConnection extends SqliteConnection {
|
||||
}> {
|
||||
const stms: InStatement[] = queries.map((q) => {
|
||||
const compiled = q.compile();
|
||||
//console.log("compiled", compiled.sql, compiled.parameters);
|
||||
return {
|
||||
sql: compiled.sql,
|
||||
args: compiled.parameters as any[]
|
||||
|
||||
@@ -18,6 +18,8 @@ export { Connection } from "./connection/Connection";
|
||||
export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlConnection";
|
||||
export { SqliteConnection } from "./connection/SqliteConnection";
|
||||
export { SqliteLocalConnection } from "./connection/SqliteLocalConnection";
|
||||
export { SqliteIntrospector } from "./connection/SqliteIntrospector";
|
||||
export { KyselyPluginRunner } from "./plugins/KyselyPluginRunner";
|
||||
|
||||
export { constructEntity, constructRelation } from "./schema/constructor";
|
||||
|
||||
|
||||
@@ -22,11 +22,7 @@ declare module "@mantine/modals" {
|
||||
}
|
||||
|
||||
export function BkndModalsProvider({ children }) {
|
||||
return (
|
||||
<ModalsProvider modals={modals} modalProps={{ className: "bknd-admin" }}>
|
||||
{children}
|
||||
</ModalsProvider>
|
||||
);
|
||||
return <ModalsProvider modals={modals}>{children}</ModalsProvider>;
|
||||
}
|
||||
|
||||
function open<Modal extends keyof typeof modals>(
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"cf-typegen": "wrangler types"
|
||||
},
|
||||
"dependencies": {
|
||||
"bknd": "workspace:*"
|
||||
"bknd": "workspace:*",
|
||||
"kysely-d1": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20241106.0",
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { serve } from "bknd/adapter/cloudflare";
|
||||
|
||||
export default serve({
|
||||
app: (args) => ({
|
||||
connection: {
|
||||
type: "libsql",
|
||||
config: {
|
||||
url: "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
}),
|
||||
onBuilt: async (app) => {
|
||||
app.modules.server.get("/custom", (c) => c.json({ hello: "world" }));
|
||||
}
|
||||
});
|
||||
export default serve();
|
||||
|
||||
@@ -10,5 +10,7 @@ assets = { directory = "../../app/dist/static" }
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
#[site]
|
||||
#bucket = "../../app/dist/static"
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "bknd-cf-example"
|
||||
database_id = "7ad67953-2bbf-47fc-8696-f4517dbfe674"
|
||||
Reference in New Issue
Block a user