mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
added aws lambda adapter + improvements to handle concurrency
This commit is contained in:
@@ -216,6 +216,7 @@ async function buildAdapters() {
|
|||||||
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("aws"));
|
||||||
await tsup.build(
|
await tsup.build(
|
||||||
baseConfig("cloudflare", {
|
baseConfig("cloudflare", {
|
||||||
external: [/^kysely/],
|
external: [/^kysely/],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.9.0-rc.1-7",
|
"version": "0.9.0-rc.1-11",
|
||||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||||
"homepage": "https://bknd.io",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -191,6 +191,11 @@
|
|||||||
"import": "./dist/adapter/astro/index.js",
|
"import": "./dist/adapter/astro/index.js",
|
||||||
"require": "./dist/adapter/astro/index.cjs"
|
"require": "./dist/adapter/astro/index.cjs"
|
||||||
},
|
},
|
||||||
|
"./adapter/aws": {
|
||||||
|
"types": "./dist/types/adapter/aws/index.d.ts",
|
||||||
|
"import": "./dist/adapter/aws/index.js",
|
||||||
|
"require": "./dist/adapter/aws/index.cjs"
|
||||||
|
},
|
||||||
"./dist/main.css": "./dist/ui/main.css",
|
"./dist/main.css": "./dist/ui/main.css",
|
||||||
"./dist/styles.css": "./dist/ui/styles.css",
|
"./dist/styles.css": "./dist/ui/styles.css",
|
||||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export class App {
|
|||||||
adminController?: AdminController;
|
adminController?: AdminController;
|
||||||
private trigger_first_boot = false;
|
private trigger_first_boot = false;
|
||||||
private plugins: AppPlugin[];
|
private plugins: AppPlugin[];
|
||||||
|
private _id: string = crypto.randomUUID();
|
||||||
|
private _building: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private connection: Connection,
|
private connection: Connection,
|
||||||
@@ -90,6 +92,11 @@ export class App {
|
|||||||
server.use(async (c, next) => {
|
server.use(async (c, next) => {
|
||||||
c.set("app", this);
|
c.set("app", this);
|
||||||
await next();
|
await next();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// gracefully add the app id
|
||||||
|
c.res.headers.set("X-bknd-id", this._id);
|
||||||
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -100,9 +107,18 @@ export class App {
|
|||||||
return this.modules.ctx().emgr;
|
return this.modules.ctx().emgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(options?: { sync?: boolean }) {
|
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
|
||||||
|
// prevent multiple concurrent builds
|
||||||
|
if (this._building) {
|
||||||
|
while (this._building) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
if (!options?.forceBuild) return;
|
||||||
|
}
|
||||||
|
this._building = true;
|
||||||
|
|
||||||
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
if (options?.sync) this.modules.ctx().flags.sync_required = true;
|
||||||
await this.modules.build();
|
await this.modules.build({ fetch: options?.fetch });
|
||||||
|
|
||||||
const { guard, server } = this.modules.ctx();
|
const { guard, server } = this.modules.ctx();
|
||||||
|
|
||||||
@@ -127,6 +143,8 @@ export class App {
|
|||||||
app: this,
|
app: this,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._building = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateConfig<Module extends keyof Modules>(module: Module) {
|
mutateConfig<Module extends keyof Modules>(module: Module) {
|
||||||
|
|||||||
68
app/src/adapter/aws/aws-lambda.adapter.ts
Normal file
68
app/src/adapter/aws/aws-lambda.adapter.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import type { App } from "bknd";
|
||||||
|
import { handle } from "hono/aws-lambda";
|
||||||
|
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
|
||||||
|
|
||||||
|
export type AwsLambdaBkndConfig = RuntimeBkndConfig & {
|
||||||
|
assets?:
|
||||||
|
| {
|
||||||
|
mode: "local";
|
||||||
|
root: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
mode: "url";
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let app: App;
|
||||||
|
export async function createApp({
|
||||||
|
adminOptions = false,
|
||||||
|
assets,
|
||||||
|
...config
|
||||||
|
}: AwsLambdaBkndConfig = {}) {
|
||||||
|
if (!app) {
|
||||||
|
let additional: Partial<RuntimeBkndConfig> = {
|
||||||
|
adminOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (assets?.mode) {
|
||||||
|
switch (assets.mode) {
|
||||||
|
case "local":
|
||||||
|
// @todo: serve static outside app context
|
||||||
|
additional = {
|
||||||
|
adminOptions: adminOptions === false ? undefined : adminOptions,
|
||||||
|
serveStatic: (await import("@hono/node-server/serve-static")).serveStatic({
|
||||||
|
root: assets.root,
|
||||||
|
onFound: (path, c) => {
|
||||||
|
c.res.headers.set("Cache-Control", "public, max-age=31536000");
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "url":
|
||||||
|
additional.adminOptions = {
|
||||||
|
...(typeof adminOptions === "object" ? adminOptions : {}),
|
||||||
|
assets_path: assets.url,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid assets mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app = await createRuntimeApp({
|
||||||
|
...config,
|
||||||
|
...additional,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serveLambda(config: AwsLambdaBkndConfig = {}) {
|
||||||
|
console.log("serving lambda");
|
||||||
|
return async (event) => {
|
||||||
|
const app = await createApp(config);
|
||||||
|
return await handle(app.server)(event);
|
||||||
|
};
|
||||||
|
}
|
||||||
1
app/src/adapter/aws/index.ts
Normal file
1
app/src/adapter/aws/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./aws-lambda.adapter";
|
||||||
@@ -34,6 +34,7 @@ export function serve({
|
|||||||
port = config.server.default_port,
|
port = config.server.default_port,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
buildConfig,
|
buildConfig,
|
||||||
|
adminOptions,
|
||||||
...serveOptions
|
...serveOptions
|
||||||
}: BunBkndConfig = {}) {
|
}: BunBkndConfig = {}) {
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
@@ -46,6 +47,7 @@ export function serve({
|
|||||||
options,
|
options,
|
||||||
onBuilt,
|
onBuilt,
|
||||||
buildConfig,
|
buildConfig,
|
||||||
|
adminOptions,
|
||||||
distPath,
|
distPath,
|
||||||
});
|
});
|
||||||
return app.fetch(request);
|
return app.fetch(request);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
|
|||||||
|
|
||||||
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
|
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
|
||||||
distPath?: string;
|
distPath?: string;
|
||||||
|
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
||||||
|
adminOptions?: AdminControllerOptions | false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): CreateAppConfig {
|
export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): CreateAppConfig {
|
||||||
@@ -55,14 +57,7 @@ export async function createFrameworkApp<Args = any>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createRuntimeApp<Env = any>(
|
export async function createRuntimeApp<Env = any>(
|
||||||
{
|
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig,
|
||||||
serveStatic,
|
|
||||||
adminOptions,
|
|
||||||
...config
|
|
||||||
}: RuntimeBkndConfig & {
|
|
||||||
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
|
|
||||||
adminOptions?: AdminControllerOptions | false;
|
|
||||||
},
|
|
||||||
env?: Env,
|
env?: Env,
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
const app = App.create(makeConfig(config, env));
|
const app = App.create(makeConfig(config, env));
|
||||||
|
|||||||
36
app/src/cli/commands/copy-assets.ts
Normal file
36
app/src/cli/commands/copy-assets.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { getRelativeDistPath } from "cli/utils/sys";
|
||||||
|
import type { CliCommand } from "../types";
|
||||||
|
import { Option } from "commander";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import c from "picocolors";
|
||||||
|
|
||||||
|
export const copyAssets: CliCommand = (program) => {
|
||||||
|
program
|
||||||
|
.command("copy-assets")
|
||||||
|
.description("copy static assets")
|
||||||
|
.addOption(new Option("-o --out <directory>", "directory to copy to"))
|
||||||
|
.addOption(new Option("-c --clean", "clean the output directory"))
|
||||||
|
.action(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function action(options: { out?: string; clean?: boolean }) {
|
||||||
|
const out = options.out ?? "static";
|
||||||
|
|
||||||
|
// clean "out" directory
|
||||||
|
if (options.clean) {
|
||||||
|
await fs.rm(out, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively copy from src/assets to out using node fs
|
||||||
|
const from = path.resolve(getRelativeDistPath(), "static");
|
||||||
|
await fs.cp(from, out, { recursive: true });
|
||||||
|
|
||||||
|
// in out, move ".vite/manifest.json" to "manifest.json"
|
||||||
|
await fs.rename(path.resolve(out, ".vite/manifest.json"), path.resolve(out, "manifest.json"));
|
||||||
|
|
||||||
|
// delete ".vite" directory in out
|
||||||
|
await fs.rm(path.resolve(out, ".vite"), { recursive: true });
|
||||||
|
|
||||||
|
console.log(c.green(`Assets copied to: ${c.bold(out)}`));
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ export { run } from "./run";
|
|||||||
export { debug } from "./debug";
|
export { debug } from "./debug";
|
||||||
export { user } from "./user";
|
export { user } from "./user";
|
||||||
export { create } from "./create";
|
export { create } from "./create";
|
||||||
|
export { copyAssets } from "./copy-assets";
|
||||||
|
|||||||
@@ -468,13 +468,18 @@ export class ModuleManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async build() {
|
async build(opts?: { fetch?: boolean }) {
|
||||||
this.logger.context("build").log("version", this.version());
|
this.logger.context("build").log("version", this.version());
|
||||||
this.logger.log("booted with", this._booted_with);
|
this.logger.log("booted with", this._booted_with);
|
||||||
|
|
||||||
// if no config provided, try fetch from db
|
// if no config provided, try fetch from db
|
||||||
if (this.version() === 0) {
|
if (this.version() === 0 || opts?.fetch === true) {
|
||||||
this.logger.context("no version").log("version is 0");
|
if (this.version() === 0) {
|
||||||
|
this.logger.context("no version").log("version is 0");
|
||||||
|
} else {
|
||||||
|
this.logger.context("force fetch").log("force fetch");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.fetch();
|
const result = await this.fetch();
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ export class SystemApi extends ModuleApi<any> {
|
|||||||
return this.get<{ version: number } & ModuleConfigs>("config");
|
return this.get<{ version: number } & ModuleConfigs>("config");
|
||||||
}
|
}
|
||||||
|
|
||||||
readSchema(options?: { config?: boolean; secrets?: boolean }) {
|
readSchema(options?: { config?: boolean; secrets?: boolean; fresh?: boolean }) {
|
||||||
return this.get<ApiSchemaResponse>("schema", {
|
return this.get<ApiSchemaResponse>("schema", {
|
||||||
config: options?.config ? 1 : 0,
|
config: options?.config ? 1 : 0,
|
||||||
secrets: options?.secrets ? 1 : 0,
|
secrets: options?.secrets ? 1 : 0,
|
||||||
|
fresh: options?.fresh ? 1 : 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,13 +164,23 @@ export class AdminController extends Controller {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isProd) {
|
if (isProd) {
|
||||||
// @ts-ignore
|
let manifest: any;
|
||||||
const manifest = await import("bknd/dist/manifest.json", {
|
if (this.options.assets_path.startsWith("http")) {
|
||||||
assert: { type: "json" },
|
manifest = await fetch(this.options.assets_path + "manifest.json", {
|
||||||
});
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
}).then((res) => res.json());
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
manifest = await import("bknd/dist/manifest.json", {
|
||||||
|
assert: { type: "json" },
|
||||||
|
}).then((res) => res.default);
|
||||||
|
}
|
||||||
|
|
||||||
// @todo: load all marked as entry (incl. css)
|
// @todo: load all marked as entry (incl. css)
|
||||||
assets.js = manifest.default["src/ui/main.tsx"].file;
|
assets.js = manifest["src/ui/main.tsx"].file;
|
||||||
assets.css = manifest.default["src/ui/main.tsx"].css[0] as any;
|
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = configs.server.admin.color_scheme ?? "light";
|
const theme = configs.server.admin.color_scheme ?? "light";
|
||||||
@@ -197,16 +207,8 @@ export class AdminController extends Controller {
|
|||||||
)}
|
)}
|
||||||
{isProd ? (
|
{isProd ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<script
|
<script type="module" src={this.options.assets_path + assets?.js} />
|
||||||
type="module"
|
<link rel="stylesheet" href={this.options.assets_path + assets?.css} />
|
||||||
CrossOrigin
|
|
||||||
src={this.options.assets_path + assets?.js}
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
crossOrigin
|
|
||||||
href={this.options.assets_path + assets?.css}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// <reference types="@cloudflare/workers-types" />
|
/// <reference types="@cloudflare/workers-types" />
|
||||||
|
|
||||||
import type { App } from "App";
|
import type { App } from "App";
|
||||||
import { tbValidator as tb } from "core";
|
import { $console, tbValidator as tb } from "core";
|
||||||
import {
|
import {
|
||||||
StringEnum,
|
StringEnum,
|
||||||
Type,
|
Type,
|
||||||
@@ -229,17 +229,23 @@ export class SystemController extends Controller {
|
|||||||
Type.Object({
|
Type.Object({
|
||||||
config: Type.Optional(booleanLike),
|
config: Type.Optional(booleanLike),
|
||||||
secrets: Type.Optional(booleanLike),
|
secrets: Type.Optional(booleanLike),
|
||||||
|
fresh: Type.Optional(booleanLike),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const module = c.req.param("module") as ModuleKey | undefined;
|
const module = c.req.param("module") as ModuleKey | undefined;
|
||||||
const { config, secrets } = c.req.valid("query");
|
const { config, secrets, fresh } = c.req.valid("query");
|
||||||
|
|
||||||
config && this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead, c);
|
config && this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead, c);
|
||||||
secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c);
|
secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c);
|
||||||
|
|
||||||
const { version, ...schema } = this.app.getSchema();
|
const { version, ...schema } = this.app.getSchema();
|
||||||
|
|
||||||
|
if (fresh) {
|
||||||
|
// in cases of concurrency, refetching schema/config must be always fresh
|
||||||
|
await this.app.build({ fetch: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (module) {
|
if (module) {
|
||||||
return c.json({
|
return c.json({
|
||||||
module,
|
module,
|
||||||
@@ -265,14 +271,18 @@ export class SystemController extends Controller {
|
|||||||
"query",
|
"query",
|
||||||
Type.Object({
|
Type.Object({
|
||||||
sync: Type.Optional(booleanLike),
|
sync: Type.Optional(booleanLike),
|
||||||
|
fetch: Type.Optional(booleanLike),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { sync } = c.req.valid("query") as Record<string, boolean>;
|
const options = c.req.valid("query") as Record<string, boolean>;
|
||||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.build, c);
|
this.ctx.guard.throwUnlessGranted(SystemPermissions.build, c);
|
||||||
|
|
||||||
await this.app.build({ sync });
|
await this.app.build(options);
|
||||||
return c.json({ success: true, options: { sync } });
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
options,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -47,19 +47,29 @@ export function BkndProvider({
|
|||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
async function reloadSchema() {
|
async function reloadSchema() {
|
||||||
await fetchSchema(includeSecrets, true);
|
await fetchSchema(includeSecrets, {
|
||||||
|
force: true,
|
||||||
|
fresh: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSchema(_includeSecrets: boolean = false, force?: boolean) {
|
async function fetchSchema(
|
||||||
|
_includeSecrets: boolean = false,
|
||||||
|
opts?: {
|
||||||
|
force?: boolean;
|
||||||
|
fresh?: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
const requesting = withSecrets ? Fetching.Secrets : Fetching.Schema;
|
const requesting = withSecrets ? Fetching.Secrets : Fetching.Schema;
|
||||||
if (fetching.current === requesting) return;
|
if (fetching.current === requesting) return;
|
||||||
|
|
||||||
if (withSecrets && !force) return;
|
if (withSecrets && opts?.force !== true) return;
|
||||||
fetching.current = requesting;
|
fetching.current = requesting;
|
||||||
|
|
||||||
const res = await api.system.readSchema({
|
const res = await api.system.readSchema({
|
||||||
config: true,
|
config: true,
|
||||||
secrets: _includeSecrets,
|
secrets: _includeSecrets,
|
||||||
|
fresh: opts?.fresh,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IconSettings } from "@tabler/icons-react";
|
import { IconRefresh, IconSettings } from "@tabler/icons-react";
|
||||||
import { ucFirst } from "core/utils";
|
import { ucFirst } from "core/utils";
|
||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { Empty } from "ui/components/display/Empty";
|
import { Empty } from "ui/components/display/Empty";
|
||||||
@@ -12,11 +12,16 @@ import { AuthSettings } from "./routes/auth.settings";
|
|||||||
import { DataSettings } from "./routes/data.settings";
|
import { DataSettings } from "./routes/data.settings";
|
||||||
import { FlowsSettings } from "./routes/flows.settings";
|
import { FlowsSettings } from "./routes/flows.settings";
|
||||||
import { ServerSettings } from "./routes/server.settings";
|
import { ServerSettings } from "./routes/server.settings";
|
||||||
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
|
|
||||||
function SettingsSidebar() {
|
function SettingsSidebar() {
|
||||||
const { version, schema } = useBknd();
|
const { version, schema, actions } = useBknd();
|
||||||
useBrowserTitle(["Settings"]);
|
useBrowserTitle(["Settings"]);
|
||||||
|
|
||||||
|
async function handleRefresh() {
|
||||||
|
await actions.reload();
|
||||||
|
}
|
||||||
|
|
||||||
const modules = Object.keys(schema).map((key) => {
|
const modules = Object.keys(schema).map((key) => {
|
||||||
return {
|
return {
|
||||||
title: schema[key].title ?? ucFirst(key),
|
title: schema[key].title ?? ucFirst(key),
|
||||||
@@ -26,7 +31,14 @@ function SettingsSidebar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell.Sidebar>
|
<AppShell.Sidebar>
|
||||||
<AppShell.SectionHeader right={<span className="font-mono">v{version}</span>}>
|
<AppShell.SectionHeader
|
||||||
|
right={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono leading-none">v{version}</span>
|
||||||
|
<IconButton Icon={IconRefresh} onClick={handleRefresh} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
Settings
|
Settings
|
||||||
</AppShell.SectionHeader>
|
</AppShell.SectionHeader>
|
||||||
<AppShell.Scrollable initialOffset={96}>
|
<AppShell.Scrollable initialOffset={96}>
|
||||||
|
|||||||
53
examples/aws-lambda/clean.sh
Executable file
53
examples/aws-lambda/clean.sh
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Set variables
|
||||||
|
FUNCTION_NAME="bknd-lambda"
|
||||||
|
ROLE_NAME="bknd-lambda-execution-role"
|
||||||
|
|
||||||
|
echo "Starting cleanup of AWS resources..."
|
||||||
|
|
||||||
|
# Delete Function URL if it exists
|
||||||
|
echo "Checking if Function URL exists for '$FUNCTION_NAME'..."
|
||||||
|
FUNCTION_URL=$(aws lambda get-function-url-config --function-name $FUNCTION_NAME --query "FunctionUrl" --output text 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$FUNCTION_URL" ]; then
|
||||||
|
echo "Deleting Function URL for '$FUNCTION_NAME'..."
|
||||||
|
aws lambda delete-function-url-config --function-name $FUNCTION_NAME
|
||||||
|
echo "Function URL deleted."
|
||||||
|
else
|
||||||
|
echo "No Function URL found for '$FUNCTION_NAME'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete Lambda function if it exists
|
||||||
|
echo "Checking if Lambda function '$FUNCTION_NAME' exists..."
|
||||||
|
LAMBDA_EXISTS=$(aws lambda get-function --function-name $FUNCTION_NAME --query "Configuration.FunctionArn" --output text 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$LAMBDA_EXISTS" ]; then
|
||||||
|
echo "Deleting Lambda function '$FUNCTION_NAME'..."
|
||||||
|
aws lambda delete-function --function-name $FUNCTION_NAME
|
||||||
|
echo "Lambda function deleted."
|
||||||
|
else
|
||||||
|
echo "Lambda function '$FUNCTION_NAME' does not exist."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete IAM role and attached policies if role exists
|
||||||
|
echo "Checking if IAM role '$ROLE_NAME' exists..."
|
||||||
|
ROLE_EXISTS=$(aws iam get-role --role-name $ROLE_NAME --query "Role.Arn" --output text 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$ROLE_EXISTS" ]; then
|
||||||
|
echo "Detaching policies from IAM role '$ROLE_NAME'..."
|
||||||
|
ATTACHED_POLICIES=$(aws iam list-attached-role-policies --role-name $ROLE_NAME --query "AttachedPolicies[].PolicyArn" --output text)
|
||||||
|
|
||||||
|
for POLICY_ARN in $ATTACHED_POLICIES; do
|
||||||
|
aws iam detach-role-policy --role-name $ROLE_NAME --policy-arn $POLICY_ARN
|
||||||
|
echo "Detached policy: $POLICY_ARN"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deleting IAM role '$ROLE_NAME'..."
|
||||||
|
aws iam delete-role --role-name $ROLE_NAME
|
||||||
|
echo "IAM role deleted."
|
||||||
|
else
|
||||||
|
echo "IAM role '$ROLE_NAME' does not exist."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "AWS resource cleanup completed successfully!"
|
||||||
131
examples/aws-lambda/deploy.sh
Executable file
131
examples/aws-lambda/deploy.sh
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Set variables
|
||||||
|
FUNCTION_NAME="bknd-lambda"
|
||||||
|
ROLE_NAME="bknd-lambda-execution-role"
|
||||||
|
RUNTIME="nodejs22.x"
|
||||||
|
HANDLER="index.handler"
|
||||||
|
ARCHITECTURE="arm64" # or "x86_64"
|
||||||
|
MEMORY="1024" # in MB, 128 is the minimum
|
||||||
|
TIMEOUT="30"
|
||||||
|
ENTRY_FILE="index.mjs"
|
||||||
|
ZIP_FILE="lambda.zip"
|
||||||
|
|
||||||
|
# Build step
|
||||||
|
echo "Building Lambda package..."
|
||||||
|
rm -rf dist && mkdir dist
|
||||||
|
|
||||||
|
# copy assets
|
||||||
|
node_modules/.bin/bknd copy-assets --out=dist/static
|
||||||
|
|
||||||
|
# Run esbuild and check for errors
|
||||||
|
# important to use --platform=browser for libsql dependency (otherwise we need to push node_modules)
|
||||||
|
if ! npx esbuild $ENTRY_FILE --bundle --format=cjs --platform=browser --external:fs --minify --outfile=dist/index.js; then
|
||||||
|
echo "Error: esbuild failed to build the package"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# zip
|
||||||
|
( cd dist && zip -r $ZIP_FILE . )
|
||||||
|
|
||||||
|
# Read .env file and export variables
|
||||||
|
if [ -f .env ]; then
|
||||||
|
export $(grep -v '^#' .env | xargs)
|
||||||
|
else
|
||||||
|
echo ".env file not found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prepare environment variables string for Lambda
|
||||||
|
ENV_VARS=$(awk -F= '{printf "%s=\"%s\",", $1, $2}' .env | sed 's/,$//')
|
||||||
|
|
||||||
|
# Create a trust policy file for the Lambda execution role
|
||||||
|
cat > trust-policy.json << EOL
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {
|
||||||
|
"Service": "lambda.amazonaws.com"
|
||||||
|
},
|
||||||
|
"Action": "sts:AssumeRole"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Create IAM role if it doesn't exist
|
||||||
|
ROLE_ARN=$(aws iam get-role --role-name $ROLE_NAME --query "Role.Arn" --output text 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$ROLE_ARN" ]; then
|
||||||
|
echo "Creating IAM role..."
|
||||||
|
aws iam create-role --role-name $ROLE_NAME --assume-role-policy-document file://trust-policy.json
|
||||||
|
aws iam attach-role-policy --role-name $ROLE_NAME --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
|
||||||
|
|
||||||
|
# Wait for IAM role to propagate
|
||||||
|
echo "Waiting for IAM role to propagate..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Get the role ARN after creation
|
||||||
|
ROLE_ARN=$(aws iam get-role --role-name $ROLE_NAME --query "Role.Arn" --output text)
|
||||||
|
else
|
||||||
|
echo "Using existing IAM role: $ROLE_ARN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create or update Lambda function
|
||||||
|
echo "Creating or updating Lambda function..."
|
||||||
|
LAMBDA_ARN=$(aws lambda get-function --function-name $FUNCTION_NAME --query "Configuration.FunctionArn" --output text 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$LAMBDA_ARN" ]; then
|
||||||
|
echo "Creating new Lambda function..."
|
||||||
|
aws lambda create-function \
|
||||||
|
--function-name $FUNCTION_NAME \
|
||||||
|
--zip-file fileb://dist/$ZIP_FILE \
|
||||||
|
--handler $HANDLER \
|
||||||
|
--runtime $RUNTIME \
|
||||||
|
--role $ROLE_ARN \
|
||||||
|
--architectures $ARCHITECTURE \
|
||||||
|
--memory-size $MEMORY \
|
||||||
|
--timeout $TIMEOUT \
|
||||||
|
--environment Variables="{$ENV_VARS}"
|
||||||
|
else
|
||||||
|
echo "Updating existing Lambda function..."
|
||||||
|
aws lambda update-function-code --function-name $FUNCTION_NAME --zip-file fileb://dist/$ZIP_FILE
|
||||||
|
|
||||||
|
echo "Waiting for Lambda function to become active..."
|
||||||
|
aws lambda wait function-updated --function-name $FUNCTION_NAME
|
||||||
|
|
||||||
|
# Update function configuration, including env variables
|
||||||
|
aws lambda update-function-configuration \
|
||||||
|
--function-name $FUNCTION_NAME \
|
||||||
|
--memory-size $MEMORY \
|
||||||
|
--timeout $TIMEOUT \
|
||||||
|
--environment Variables="{$ENV_VARS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Function URL exists, if not create it
|
||||||
|
echo "Checking if Function URL exists..."
|
||||||
|
FUNCTION_URL=$(aws lambda get-function-url-config --function-name $FUNCTION_NAME --query "FunctionUrl" --output text 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$FUNCTION_URL" ]; then
|
||||||
|
echo "Creating Function URL..."
|
||||||
|
FUNCTION_URL=$(aws lambda create-function-url-config \
|
||||||
|
--function-name $FUNCTION_NAME \
|
||||||
|
--auth-type NONE \
|
||||||
|
--query "FunctionUrl" --output text)
|
||||||
|
|
||||||
|
# Make the Function URL publicly accessible (log output)
|
||||||
|
aws lambda add-permission \
|
||||||
|
--function-name $FUNCTION_NAME \
|
||||||
|
--action lambda:InvokeFunctionUrl \
|
||||||
|
--principal "*" \
|
||||||
|
--statement-id public-access \
|
||||||
|
--function-url-auth-type NONE
|
||||||
|
|
||||||
|
echo "Created Lambda Function URL: $FUNCTION_URL"
|
||||||
|
else
|
||||||
|
echo "Lambda Function URL: $FUNCTION_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployment completed successfully!"
|
||||||
14
examples/aws-lambda/index.mjs
Normal file
14
examples/aws-lambda/index.mjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { serveLambda } from "bknd/adapter/aws";
|
||||||
|
|
||||||
|
export const handler = serveLambda({
|
||||||
|
// to get local assets, run `npx bknd copy-assets`
|
||||||
|
// this is automatically done in `deploy.sh`
|
||||||
|
assets: {
|
||||||
|
mode: "local",
|
||||||
|
root: "./static",
|
||||||
|
},
|
||||||
|
connection: {
|
||||||
|
url: process.env.DB_URL,
|
||||||
|
authToken: process.env.DB_TOKEN,
|
||||||
|
},
|
||||||
|
});
|
||||||
21
examples/aws-lambda/package.json
Normal file
21
examples/aws-lambda/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "aws-lambda",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"scripts": {
|
||||||
|
"test": "esbuild index.mjs --bundle --format=cjs --platform=node --external:fs --outfile=dist/index.js && node test.js",
|
||||||
|
"deploy": "./deploy.sh",
|
||||||
|
"clean": "./clean.sh"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"bknd": "file:../../app/bknd-0.9.0-rc.1-11.tgz"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"dotenv": "^16.4.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
examples/aws-lambda/test.js
Normal file
31
examples/aws-lambda/test.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
require("dotenv").config();
|
||||||
|
const handler = require("./dist/index.js").handler;
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
httpMethod: "GET",
|
||||||
|
path: "/",
|
||||||
|
//path: "/api/system/config",
|
||||||
|
//path: "/assets/main-B6sEDlfs.js",
|
||||||
|
headers: {
|
||||||
|
//"Content-Type": "application/json",
|
||||||
|
"User-Agent": "curl/7.64.1",
|
||||||
|
Accept: "*/*",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
awsRequestId: "mocked-request-id",
|
||||||
|
functionName: "myMinimalLambda",
|
||||||
|
functionVersion: "$LATEST",
|
||||||
|
memoryLimitInMB: "128",
|
||||||
|
getRemainingTimeInMillis: () => 5000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the handler
|
||||||
|
handler(event, context)
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.statusCode, response.body);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
});
|
||||||
12
examples/aws-lambda/trust-policy.json
Normal file
12
examples/aws-lambda/trust-policy.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {
|
||||||
|
"Service": "lambda.amazonaws.com"
|
||||||
|
},
|
||||||
|
"Action": "sts:AssumeRole"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
examples/bun/static.ts
Normal file
17
examples/bun/static.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// @ts-ignore somehow causes types:build issues on app
|
||||||
|
import { type BunBkndConfig, serve } from "bknd/adapter/bun";
|
||||||
|
|
||||||
|
// Actually, all it takes is the following line:
|
||||||
|
// serve();
|
||||||
|
|
||||||
|
// this is optional, if omitted, it uses an in-memory database
|
||||||
|
const config: BunBkndConfig = {
|
||||||
|
adminOptions: {
|
||||||
|
assets_path: "https://cdn.bknd.io/0.9.0-rc.1/",
|
||||||
|
},
|
||||||
|
connection: {
|
||||||
|
url: "file:data.db",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
serve(config);
|
||||||
Reference in New Issue
Block a user