Merge pull request #73 from bknd-io/feat/cf-d1-and-r2

adding d1 and r2 for cloudflare environments
This commit is contained in:
dswbx
2025-02-12 09:04:47 +01:00
committed by GitHub
20 changed files with 387 additions and 175 deletions

View File

@@ -49,6 +49,7 @@ if (types && !watch) {
/** /**
* Building backend and general API * Building backend and general API
*/ */
async function buildApi() {
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
@@ -68,10 +69,12 @@ await tsup.build({
delayTypes(); delayTypes();
} }
}); });
}
/** /**
* Building UI for direct imports * Building UI for direct imports
*/ */
async function buildUi() {
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
@@ -105,12 +108,14 @@ await tsup.build({
delayTypes(); delayTypes();
} }
}); });
}
/** /**
* Building UI Elements * Building UI Elements
* - tailwind-merge is mocked, no exclude * - tailwind-merge is mocked, no exclude
* - ui/client is external, and after built replaced with "bknd/client" * - ui/client is external, and after built replaced with "bknd/client"
*/ */
async function buildUiElements() {
await tsup.build({ await tsup.build({
minify, minify,
sourcemap, sourcemap,
@@ -149,11 +154,12 @@ await tsup.build({
delayTypes(); delayTypes();
} }
}); });
}
/** /**
* Building adapters * Building adapters
*/ */
function baseConfig(adapter: string): tsup.Options { function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsup.Options {
return { return {
minify, minify,
sourcemap, sourcemap,
@@ -162,23 +168,27 @@ function baseConfig(adapter: string): tsup.Options {
format: ["esm"], format: ["esm"],
platform: "neutral", platform: "neutral",
outDir: `dist/adapter/${adapter}`, outDir: `dist/adapter/${adapter}`,
metafile: true,
splitting: false,
onSuccess: async () => {
delayTypes();
},
...overrides,
define: { define: {
__isDev: "0" __isDev: "0",
...overrides.define
}, },
external: [ external: [
/^cloudflare*/, /^cloudflare*/,
/^@?(hono|libsql).*?/, /^@?(hono|libsql).*?/,
/^(bknd|react|next|node).*?/, /^(bknd|react|next|node).*?/,
/.*\.(html)$/ /.*\.(html)$/,
], ...(Array.isArray(overrides.external) ? overrides.external : [])
metafile: true, ]
splitting: false,
onSuccess: async () => {
delayTypes();
}
}; };
} }
async function buildAdapters() {
// base adapter handles // base adapter handles
await tsup.build({ await tsup.build({
...baseConfig(""), ...baseConfig(""),
@@ -190,7 +200,11 @@ await tsup.build({
await tsup.build(baseConfig("remix")); await tsup.build(baseConfig("remix"));
await tsup.build(baseConfig("bun")); await tsup.build(baseConfig("bun"));
await tsup.build(baseConfig("astro")); await tsup.build(baseConfig("astro"));
await tsup.build(baseConfig("cloudflare")); await tsup.build(
baseConfig("cloudflare", {
external: [/^kysely/]
})
);
await tsup.build({ await tsup.build({
...baseConfig("vite"), ...baseConfig("vite"),
@@ -206,3 +220,9 @@ await tsup.build({
...baseConfig("node"), ...baseConfig("node"),
platform: "node" platform: "node"
}); });
}
await buildApi();
await buildUi();
await buildUiElements();
await buildAdapters();

View File

@@ -76,6 +76,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"esbuild-postcss": "^0.0.4", "esbuild-postcss": "^0.0.4",
"jotai": "^2.10.1", "jotai": "^2.10.1",
"kysely-d1": "^0.3.0",
"open": "^10.1.0", "open": "^10.1.0",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"postcss": "^8.4.47", "postcss": "^8.4.47",

View File

@@ -0,0 +1,63 @@
/// <reference types="@cloudflare/workers-types" />
import { KyselyPluginRunner, SqliteConnection, SqliteIntrospector } from "bknd/data";
import type { QB } from "data/connection/Connection";
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: ["_cf_KV"]
});
}
}
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,6 +1,47 @@
import { isDebug } from "core"; import { registries } from "bknd";
import type { FileBody, StorageAdapter } from "../Storage"; import { isDebug } from "bknd/core";
import { guessMimeType } from "../mime-types"; import { StringEnum, Type } from "bknd/utils";
import type { FileBody, StorageAdapter } from "media/storage/Storage";
import { guess } from "media/storage/mime-types-tiny";
import { getBindings } from "./bindings";
export function makeSchema(bindings: string[] = []) {
return Type.Object(
{
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String())
},
{ title: "R2", description: "Cloudflare R2 storage" }
);
}
export function registerMedia(env: Record<string, any>) {
const r2_bindings = getBindings(env, "R2Bucket");
registries.media.register(
"r2",
class extends StorageR2Adapter {
constructor(private config: any) {
const binding = r2_bindings.find((b) => b.key === config.binding);
if (!binding) {
throw new Error(`No R2Bucket found with key ${config.binding}`);
}
super(binding?.value);
}
override getSchema() {
return makeSchema(r2_bindings.map((b) => b.key));
}
override toJSON() {
return {
...super.toJSON(),
config: this.config
};
}
}
);
}
/** /**
* Adapter for R2 storage * Adapter for R2 storage
@@ -14,7 +55,7 @@ export class StorageR2Adapter implements StorageAdapter {
} }
getSchema() { getSchema() {
return undefined; return makeSchema();
} }
async putObject(key: string, body: FileBody) { async putObject(key: string, body: FileBody) {
@@ -47,7 +88,8 @@ export class StorageR2Adapter implements StorageAdapter {
async getObject(key: string, headers: Headers): Promise<Response> { async getObject(key: string, headers: Headers): Promise<Response> {
let object: R2ObjectBody | null; let object: R2ObjectBody | null;
const responseHeaders = new Headers({ const responseHeaders = new Headers({
"Accept-Ranges": "bytes" "Accept-Ranges": "bytes",
"Content-Type": guess(key)
}); });
//console.log("getObject:headers", headersToObject(headers)); //console.log("getObject:headers", headersToObject(headers));
@@ -97,10 +139,9 @@ export class StorageR2Adapter implements StorageAdapter {
if (!metadata || Object.keys(metadata).length === 0) { if (!metadata || Object.keys(metadata).length === 0) {
// guessing is especially required for dev environment (miniflare) // guessing is especially required for dev environment (miniflare)
metadata = { metadata = {
contentType: guessMimeType(object.key) contentType: guess(object.key)
}; };
} }
//console.log("writeHttpMetadata", object.httpMetadata, metadata);
for (const [key, value] of Object.entries(metadata)) { for (const [key, value] of Object.entries(metadata)) {
const camelToDash = key.replace(/([A-Z])/g, "-$1").toLowerCase(); const camelToDash = key.replace(/([A-Z])/g, "-$1").toLowerCase();
@@ -115,7 +156,7 @@ export class StorageR2Adapter implements StorageAdapter {
} }
return { return {
type: String(head.httpMetadata?.contentType ?? "application/octet-stream"), type: String(head.httpMetadata?.contentType ?? guess(key)),
size: head.size size: head.size
}; };
} }

View File

@@ -0,0 +1,32 @@
export type BindingTypeMap = {
D1Database: D1Database;
KVNamespace: KVNamespace;
DurableObjectNamespace: DurableObjectNamespace;
R2Bucket: R2Bucket;
};
export type GetBindingType = keyof BindingTypeMap;
export type BindingMap<T extends GetBindingType> = { key: string; value: BindingTypeMap[T] };
export function getBindings<T extends GetBindingType>(env: any, type: T): BindingMap<T>[] {
const bindings: BindingMap<T>[] = [];
for (const key in env) {
try {
if (env[key] && (env[key] as any).constructor.name === type) {
bindings.push({
key,
value: env[key] as BindingTypeMap[T]
});
}
} catch (e) {}
}
return bindings;
}
export function getBinding<T extends GetBindingType>(env: any, type: T): BindingMap<T> {
const bindings = getBindings(env, type);
if (bindings.length === 0) {
throw new Error(`No ${type} found in bindings`);
}
return bindings[0] as BindingMap<T>;
}

View File

@@ -1,6 +1,11 @@
import type { FrameworkBkndConfig } from "bknd/adapter"; /// <reference types="@cloudflare/workers-types" />
import { type FrameworkBkndConfig, makeConfig } from "bknd/adapter";
import { Hono } from "hono"; import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers"; import { serveStatic } from "hono/cloudflare-workers";
import { D1Connection } from "./D1Connection";
import { registerMedia } from "./StorageR2Adapter";
import { getBinding } from "./bindings";
import { getCached } from "./modes/cached"; import { getCached } from "./modes/cached";
import { getDurable } from "./modes/durable"; import { getDurable } from "./modes/durable";
import { getFresh, getWarm } from "./modes/fresh"; import { getFresh, getWarm } from "./modes/fresh";
@@ -10,6 +15,7 @@ export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>>
bindings?: (args: Context<Env>) => { bindings?: (args: Context<Env>) => {
kv?: KVNamespace; kv?: KVNamespace;
dobj?: DurableObjectNamespace; dobj?: DurableObjectNamespace;
db?: D1Database;
}; };
static?: "kv" | "assets"; static?: "kv" | "assets";
key?: string; key?: string;
@@ -26,7 +32,39 @@ export type Context<Env = any> = {
ctx: ExecutionContext; ctx: ExecutionContext;
}; };
export function serve<Env = any>(config: CloudflareBkndConfig<Env>) { let media_registered: boolean = false;
export function makeCfConfig(config: CloudflareBkndConfig, context: Context) {
if (!media_registered) {
registerMedia(context.env as any);
media_registered = true;
}
const appConfig = makeConfig(config, context);
const bindings = config.bindings?.(context);
if (!appConfig.connection) {
let db: D1Database | undefined;
if (bindings?.db) {
console.log("Using database from bindings");
db = bindings.db;
} else if (Object.keys(context.env ?? {}).length > 0) {
const binding = getBinding(context.env, "D1Database");
if (binding) {
console.log(`Using database from env "${binding.key}"`);
db = binding.value;
}
}
if (db) {
appConfig.connection = new D1Connection({ binding: db });
} else {
throw new Error("No database connection given");
}
}
return appConfig;
}
export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
return { return {
async fetch(request: Request, env: Env, ctx: ExecutionContext) { async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url); const url = new URL(request.url);
@@ -61,8 +99,6 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env>) {
} }
} }
config.setAdminHtml = config.setAdminHtml && !!config.manifest;
const context = { request, env, ctx } as Context; const context = { request, env, ctx } as Context;
const mode = config.mode ?? "warm"; const mode = config.mode ?? "warm";

View File

@@ -1,4 +1,18 @@
import { D1Connection, type D1ConnectionConfig } from "./D1Connection";
export * from "./cloudflare-workers.adapter"; export * from "./cloudflare-workers.adapter";
export { makeApp, getFresh, getWarm } from "./modes/fresh"; export { makeApp, getFresh, getWarm } from "./modes/fresh";
export { getCached } from "./modes/cached"; export { getCached } from "./modes/cached";
export { DurableBkndApp, getDurable } from "./modes/durable"; export { DurableBkndApp, getDurable } from "./modes/durable";
export { D1Connection, type D1ConnectionConfig };
export {
getBinding,
getBindings,
type BindingTypeMap,
type GetBindingType,
type BindingMap
} from "./bindings";
export function d1(config: D1ConnectionConfig) {
return new D1Connection(config);
}

View File

@@ -1,6 +1,6 @@
import { App } from "bknd"; import { App } from "bknd";
import { createRuntimeApp } from "bknd/adapter"; import { createRuntimeApp } from "bknd/adapter";
import type { CloudflareBkndConfig, Context } from "../index"; import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...args }: Context) { export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...args }: Context) {
const { kv } = config.bindings?.(env)!; const { kv } = config.bindings?.(env)!;
@@ -16,7 +16,7 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
const app = await createRuntimeApp( const app = await createRuntimeApp(
{ {
...config, ...makeCfConfig(config, { env, ctx, ...args }),
initialConfig, initialConfig,
onBuilt: async (app) => { onBuilt: async (app) => {
app.module.server.client.get("/__bknd/cache", async (c) => { app.module.server.client.get("/__bknd/cache", async (c) => {

View File

@@ -1,11 +1,11 @@
import type { App } from "bknd"; import type { App } from "bknd";
import { createRuntimeApp } from "bknd/adapter"; import { createRuntimeApp } from "bknd/adapter";
import type { CloudflareBkndConfig, Context } from "../index"; import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
export async function makeApp(config: CloudflareBkndConfig, ctx: Context) { export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
return await createRuntimeApp( return await createRuntimeApp(
{ {
...config, ...makeCfConfig(config, ctx),
adminOptions: config.html ? { html: config.html } : undefined adminOptions: config.html ? { html: config.html } : undefined
}, },
ctx ctx

View File

@@ -74,7 +74,6 @@ export class LibsqlConnection extends SqliteConnection {
}> { }> {
const stms: InStatement[] = queries.map((q) => { const stms: InStatement[] = queries.map((q) => {
const compiled = q.compile(); const compiled = q.compile();
//console.log("compiled", compiled.sql, compiled.parameters);
return { return {
sql: compiled.sql, sql: compiled.sql,
args: compiled.parameters as any[] args: compiled.parameters as any[]

View File

@@ -5,7 +5,7 @@ import type {
ExpressionBuilder, ExpressionBuilder,
Kysely, Kysely,
SchemaMetadata, SchemaMetadata,
TableMetadata, TableMetadata
} from "kysely"; } from "kysely";
import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely"; import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely";
import type { ConnectionIntrospector, IndexMetadata } from "./Connection"; import type { ConnectionIntrospector, IndexMetadata } from "./Connection";
@@ -62,7 +62,7 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
seqno: number; seqno: number;
cid: number; cid: number;
name: string; name: string;
}>`pragma_index_info(${index})`.as("index_info"), }>`pragma_index_info(${index})`.as("index_info")
) )
.select(["seqno", "cid", "name"]) .select(["seqno", "cid", "name"])
.orderBy("cid") .orderBy("cid")
@@ -74,8 +74,8 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
isUnique: isUnique, isUnique: isUnique,
columns: columns.map((col) => ({ columns: columns.map((col) => ({
name: col.name, name: col.name,
order: col.seqno, order: col.seqno
})), }))
}; };
} }
@@ -87,7 +87,7 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
} }
async getTables( async getTables(
options: DatabaseMetadataOptions = { withInternalKyselyTables: false }, options: DatabaseMetadataOptions = { withInternalKyselyTables: false }
): Promise<TableMetadata[]> { ): Promise<TableMetadata[]> {
let query = this.#db let query = this.#db
.selectFrom("sqlite_master") .selectFrom("sqlite_master")
@@ -99,7 +99,7 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
if (!options.withInternalKyselyTables) { if (!options.withInternalKyselyTables) {
query = query.where( query = query.where(
this.excludeTables([DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK_TABLE]), this.excludeTables([DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK_TABLE])
); );
} }
if (this._excludeTables.length > 0) { if (this._excludeTables.length > 0) {
@@ -107,17 +107,19 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
} }
const tables = await query.execute(); const tables = await query.execute();
console.log("tables", tables);
return Promise.all(tables.map(({ name }) => this.#getTableMetadata(name))); return Promise.all(tables.map(({ name }) => this.#getTableMetadata(name)));
} }
async getMetadata(options?: DatabaseMetadataOptions): Promise<DatabaseMetadata> { async getMetadata(options?: DatabaseMetadataOptions): Promise<DatabaseMetadata> {
return { return {
tables: await this.getTables(options), tables: await this.getTables(options)
}; };
} }
async #getTableMetadata(table: string): Promise<TableMetadata> { async #getTableMetadata(table: string): Promise<TableMetadata> {
const db = this.#db; const db = this.#db;
console.log("get table metadata", table);
// Get the SQL that was used to create the table. // Get the SQL that was used to create the table.
const tableDefinition = await db const tableDefinition = await db
@@ -142,7 +144,7 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
type: string; type: string;
notnull: 0 | 1; notnull: 0 | 1;
dflt_value: any; dflt_value: any;
}>`pragma_table_info(${table})`.as("table_info"), }>`pragma_table_info(${table})`.as("table_info")
) )
.select(["name", "type", "notnull", "dflt_value"]) .select(["name", "type", "notnull", "dflt_value"])
.orderBy("cid") .orderBy("cid")
@@ -157,8 +159,8 @@ export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntro
isNullable: !col.notnull, isNullable: !col.notnull,
isAutoIncrementing: col.name === autoIncrementCol, isAutoIncrementing: col.name === autoIncrementCol,
hasDefaultValue: col.dflt_value != null, hasDefaultValue: col.dflt_value != null,
comment: undefined, comment: undefined
})), }))
}; };
} }
} }

View File

@@ -18,6 +18,8 @@ export { Connection } from "./connection/Connection";
export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlConnection"; export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlConnection";
export { SqliteConnection } from "./connection/SqliteConnection"; export { SqliteConnection } from "./connection/SqliteConnection";
export { SqliteLocalConnection } from "./connection/SqliteLocalConnection"; export { SqliteLocalConnection } from "./connection/SqliteLocalConnection";
export { SqliteIntrospector } from "./connection/SqliteIntrospector";
export { KyselyPluginRunner } from "./plugins/KyselyPluginRunner";
export { constructEntity, constructRelation } from "./schema/constructor"; export { constructEntity, constructRelation } from "./schema/constructor";

View File

@@ -40,7 +40,8 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
let adapter: StorageAdapter; let adapter: StorageAdapter;
try { try {
const { type, config } = this.config.adapter; const { type, config } = this.config.adapter;
adapter = new (registry.get(type as any).cls)(config as any); const cls = registry.get(type as any).cls;
adapter = new cls(config as any);
this._storage = new Storage(adapter, this.config.storage, this.ctx.emgr); this._storage = new Storage(adapter, this.config.storage, this.ctx.emgr);
this.setBuilt(); this.setBuilt();
@@ -53,8 +54,6 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
index(media).on(["path"], true).on(["reference"]); index(media).on(["path"], true).on(["reference"]);
}) })
); );
this.setBuilt();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
throw new Error( throw new Error(

View File

@@ -16,8 +16,8 @@ export function buildMediaSchema() {
config: adapter.schema config: adapter.schema
}, },
{ {
title: adapter.schema.title ?? name, title: adapter.schema?.title ?? name,
description: adapter.schema.description, description: adapter.schema?.description,
additionalProperties: false additionalProperties: false
} }
); );

View File

@@ -22,11 +22,7 @@ declare module "@mantine/modals" {
} }
export function BkndModalsProvider({ children }) { export function BkndModalsProvider({ children }) {
return ( return <ModalsProvider modals={modals}>{children}</ModalsProvider>;
<ModalsProvider modals={modals} modalProps={{ className: "bknd-admin" }}>
{children}
</ModalsProvider>
);
} }
function open<Modal extends keyof typeof modals>( function open<Modal extends keyof typeof modals>(

View File

@@ -1,4 +1,4 @@
import { IconBrandAws, IconCloud, IconServer } from "@tabler/icons-react"; import { IconBrandAws, IconBrandCloudflare, IconCloud, IconServer } from "@tabler/icons-react";
import { isDebug } from "core"; import { isDebug } from "core";
import { autoFormatString } from "core/utils"; import { autoFormatString } from "core/utils";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -113,10 +113,15 @@ const RootFormError = () => {
); );
}; };
const Icons = [IconBrandAws, IconCloud, IconServer]; const Icons = {
s3: IconBrandAws,
cloudinary: IconCloud,
local: IconServer,
r2: IconBrandCloudflare
};
const AdapterIcon = ({ index }: { index: number }) => { const AdapterIcon = ({ type }: { type: string }) => {
const Icon = Icons[index]; const Icon = Icons[type];
if (!Icon) return null; if (!Icon) return null;
return <Icon />; return <Icon />;
}; };
@@ -142,7 +147,7 @@ function Adapters() {
)} )}
> >
<div> <div>
<AdapterIcon index={i} /> <AdapterIcon type={schema.properties.type.const} />
</div> </div>
<div className="flex flex-col items-start justify-center"> <div className="flex flex-col items-start justify-center">
<span>{autoFormatString(schema.title)}</span> <span>{autoFormatString(schema.title)}</span>

BIN
bun.lockb

Binary file not shown.

View File

@@ -11,7 +11,8 @@
"cf-typegen": "wrangler types" "cf-typegen": "wrangler types"
}, },
"dependencies": { "dependencies": {
"bknd": "workspace:*" "bknd": "workspace:*",
"kysely-d1": "^0.3.0"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20241106.0", "@cloudflare/workers-types": "^4.20241106.0",

View File

@@ -1,14 +1,9 @@
/// <reference types="@cloudflare/workers-types" />
import { serve } from "bknd/adapter/cloudflare"; import { serve } from "bknd/adapter/cloudflare";
export default serve({ export default serve({
app: (args) => ({ mode: "fresh",
connection: {
type: "libsql",
config: {
url: "http://localhost:8080"
}
}
}),
onBuilt: async (app) => { onBuilt: async (app) => {
app.modules.server.get("/custom", (c) => c.json({ hello: "world" })); app.modules.server.get("/custom", (c) => c.json({ hello: "world" }));
} }

View File

@@ -1,7 +1,7 @@
#:schema node_modules/wrangler/config-schema.json #:schema node_modules/wrangler/config-schema.json
name = "bknd-cf-worker-example" name = "bknd-cf-worker-example"
main = "src/index.ts" main = "src/index.ts"
compatibility_date = "2024-11-06" compatibility_date = "2025-02-04"
compatibility_flags = ["nodejs_compat"] compatibility_flags = ["nodejs_compat"]
workers_dev = true workers_dev = true
minify = true minify = true
@@ -10,5 +10,11 @@ assets = { directory = "../../app/dist/static" }
[observability] [observability]
enabled = true enabled = true
#[site] [[d1_databases]]
#bucket = "../../app/dist/static" binding = "DB"
database_name = "bknd-cf-example"
database_id = "7ad67953-2bbf-47fc-8696-f4517dbfe674"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "bknd-cf-example"