adding d1 for cloudflare environments

This commit is contained in:
dswbx
2025-02-10 17:46:22 +01:00
parent 1e5c0dbc22
commit be39d1c374
12 changed files with 264 additions and 151 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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[]

View File

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

View File

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

BIN
bun.lockb

Binary file not shown.

View File

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

View File

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

View File

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