added r2 binding support to cf adapter

This commit is contained in:
dswbx
2025-02-11 10:04:53 +01:00
parent be39d1c374
commit 7a6321f4ce
12 changed files with 157 additions and 61 deletions

View File

@@ -1,9 +1,7 @@
/// <reference types="@cloudflare/workers-types" />
import { SqliteConnection } from "bknd/data";
import { KyselyPluginRunner } from "data";
import { KyselyPluginRunner, SqliteConnection, SqliteIntrospector } from "bknd/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";

View File

@@ -1,6 +1,47 @@
import { isDebug } from "core";
import type { FileBody, StorageAdapter } from "../Storage";
import { guessMimeType } from "../mime-types";
import { registries } from "bknd";
import { isDebug } from "bknd/core";
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
@@ -14,7 +55,7 @@ export class StorageR2Adapter implements StorageAdapter {
}
getSchema() {
return undefined;
return makeSchema();
}
async putObject(key: string, body: FileBody) {
@@ -47,7 +88,8 @@ export class StorageR2Adapter implements StorageAdapter {
async getObject(key: string, headers: Headers): Promise<Response> {
let object: R2ObjectBody | null;
const responseHeaders = new Headers({
"Accept-Ranges": "bytes"
"Accept-Ranges": "bytes",
"Content-Type": guess(key)
});
//console.log("getObject:headers", headersToObject(headers));
@@ -97,10 +139,9 @@ export class StorageR2Adapter implements StorageAdapter {
if (!metadata || Object.keys(metadata).length === 0) {
// guessing is especially required for dev environment (miniflare)
metadata = {
contentType: guessMimeType(object.key)
contentType: guess(object.key)
};
}
//console.log("writeHttpMetadata", object.httpMetadata, metadata);
for (const [key, value] of Object.entries(metadata)) {
const camelToDash = key.replace(/([A-Z])/g, "-$1").toLowerCase();
@@ -115,7 +156,7 @@ export class StorageR2Adapter implements StorageAdapter {
}
return {
type: String(head.httpMetadata?.contentType ?? "application/octet-stream"),
type: String(head.httpMetadata?.contentType ?? guess(key)),
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

@@ -3,7 +3,9 @@
import { type FrameworkBkndConfig, makeConfig } from "bknd/adapter";
import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers";
import { D1Connection } from "./connection/D1Connection";
import { D1Connection } from "./D1Connection";
import { registerMedia } from "./StorageR2Adapter";
import { getBinding } from "./bindings";
import { getCached } from "./modes/cached";
import { getDurable } from "./modes/durable";
import { getFresh, getWarm } from "./modes/fresh";
@@ -30,6 +32,38 @@ export type Context<Env = any> = {
ctx: ExecutionContext;
};
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 {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
@@ -68,43 +102,15 @@ export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
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(appConfig, context);
return await getFresh(config, context);
case "warm":
return await getWarm(appConfig, context);
return await getWarm(config, context);
case "cache":
return await getCached(appConfig, context);
return await getCached(config, context);
case "durable":
return await getDurable(appConfig, context);
return await getDurable(config, context);
default:
throw new Error(`Unknown mode ${mode}`);
}

View File

@@ -1,10 +1,17 @@
import { D1Connection, type D1ConnectionConfig } from "./connection/D1Connection";
import { D1Connection, type D1ConnectionConfig } from "./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 {
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 { 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) {
const { kv } = config.bindings?.(env)!;
@@ -16,7 +16,7 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
const app = await createRuntimeApp(
{
...config,
...makeCfConfig(config, { env, ctx, ...args }),
initialConfig,
onBuilt: async (app) => {
app.module.server.client.get("/__bknd/cache", async (c) => {

View File

@@ -1,11 +1,11 @@
import type { App } from "bknd";
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) {
return await createRuntimeApp(
{
...config,
...makeCfConfig(config, ctx),
adminOptions: config.html ? { html: config.html } : undefined
},
ctx

View File

@@ -40,7 +40,8 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
let adapter: StorageAdapter;
try {
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.setBuilt();
@@ -53,8 +54,6 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
index(media).on(["path"], true).on(["reference"]);
})
);
this.setBuilt();
} catch (e) {
console.error(e);
throw new Error(

View File

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

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 { autoFormatString } from "core/utils";
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 Icon = Icons[index];
const AdapterIcon = ({ type }: { type: string }) => {
const Icon = Icons[type];
if (!Icon) return null;
return <Icon />;
};
@@ -142,7 +147,7 @@ function Adapters() {
)}
>
<div>
<AdapterIcon index={i} />
<AdapterIcon type={schema.properties.type.const} />
</div>
<div className="flex flex-col items-start justify-center">
<span>{autoFormatString(schema.title)}</span>