diff --git a/app/build.ts b/app/build.ts index ebbf33a..bbd4631 100644 --- a/app/build.ts +++ b/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 { 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(); diff --git a/app/package.json b/app/package.json index 71ca15f..c4f4f56 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts index 3a4044b..d12d02b 100644 --- a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts +++ b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts @@ -1,6 +1,9 @@ -import type { FrameworkBkndConfig } from "bknd/adapter"; +/// + +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 = FrameworkBkndConfig> bindings?: (args: Context) => { kv?: KVNamespace; dobj?: DurableObjectNamespace; + db?: D1Database; }; static?: "kv" | "assets"; key?: string; @@ -26,7 +30,7 @@ export type Context = { ctx: ExecutionContext; }; -export function serve(config: CloudflareBkndConfig) { +export function serve(config: CloudflareBkndConfig = {}) { return { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const url = new URL(request.url); @@ -61,20 +65,46 @@ export function serve(config: CloudflareBkndConfig) { } } - 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}`); } diff --git a/app/src/adapter/cloudflare/connection/D1Connection.ts b/app/src/adapter/cloudflare/connection/D1Connection.ts new file mode 100644 index 0000000..d47458d --- /dev/null +++ b/app/src/adapter/cloudflare/connection/D1Connection.ts @@ -0,0 +1,65 @@ +/// + +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): 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: [...Queries] + ): Promise<{ + [K in keyof Queries]: Awaited>; + }> { + 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; + } +} diff --git a/app/src/adapter/cloudflare/index.ts b/app/src/adapter/cloudflare/index.ts index c2dd1c5..a173c2e 100644 --- a/app/src/adapter/cloudflare/index.ts +++ b/app/src/adapter/cloudflare/index.ts @@ -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); +} diff --git a/app/src/data/connection/LibsqlConnection.ts b/app/src/data/connection/LibsqlConnection.ts index e60fa32..ab7db32 100644 --- a/app/src/data/connection/LibsqlConnection.ts +++ b/app/src/data/connection/LibsqlConnection.ts @@ -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[] diff --git a/app/src/data/index.ts b/app/src/data/index.ts index a5de079..e63707e 100644 --- a/app/src/data/index.ts +++ b/app/src/data/index.ts @@ -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"; diff --git a/app/src/ui/modals/index.tsx b/app/src/ui/modals/index.tsx index 9869158..9b15848 100644 --- a/app/src/ui/modals/index.tsx +++ b/app/src/ui/modals/index.tsx @@ -22,11 +22,7 @@ declare module "@mantine/modals" { } export function BkndModalsProvider({ children }) { - return ( - - {children} - - ); + return {children}; } function open( diff --git a/bun.lockb b/bun.lockb index 87d85d6..ff10b8a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/cloudflare-worker/package.json b/examples/cloudflare-worker/package.json index 689ee95..55526c4 100644 --- a/examples/cloudflare-worker/package.json +++ b/examples/cloudflare-worker/package.json @@ -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", diff --git a/examples/cloudflare-worker/src/index.ts b/examples/cloudflare-worker/src/index.ts index 628c490..5d28a61 100644 --- a/examples/cloudflare-worker/src/index.ts +++ b/examples/cloudflare-worker/src/index.ts @@ -1,15 +1,5 @@ +/// + 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(); diff --git a/examples/cloudflare-worker/wrangler.toml b/examples/cloudflare-worker/wrangler.toml index 27157d0..7f049ef 100644 --- a/examples/cloudflare-worker/wrangler.toml +++ b/examples/cloudflare-worker/wrangler.toml @@ -10,5 +10,7 @@ assets = { directory = "../../app/dist/static" } [observability] enabled = true -#[site] -#bucket = "../../app/dist/static" \ No newline at end of file +[[d1_databases]] +binding = "DB" +database_name = "bknd-cf-example" +database_id = "7ad67953-2bbf-47fc-8696-f4517dbfe674" \ No newline at end of file