diff --git a/app/build.ts b/app/build.ts index ab66688..faa81d0 100644 --- a/app/build.ts +++ b/app/build.ts @@ -179,3 +179,8 @@ await tsup.build({ platform: "node", format: ["esm", "cjs"] }); + +await tsup.build({ + ...baseConfig("astro"), + format: ["esm", "cjs"] +}); diff --git a/app/package.json b/app/package.json index 2a1ae80..e024279 100644 --- a/app/package.json +++ b/app/package.json @@ -160,6 +160,11 @@ "import": "./dist/adapter/node/index.js", "require": "./dist/adapter/node/index.cjs" }, + "./adapter/astro": { + "types": "./dist/adapter/astro/index.d.ts", + "import": "./dist/adapter/astro/index.js", + "require": "./dist/adapter/astro/index.cjs" + }, "./dist/styles.css": "./dist/ui/main.css", "./dist/manifest.json": "./dist/static/manifest.json" }, diff --git a/app/src/adapter/astro/astro.adapter.ts b/app/src/adapter/astro/astro.adapter.ts new file mode 100644 index 0000000..6076750 --- /dev/null +++ b/app/src/adapter/astro/astro.adapter.ts @@ -0,0 +1,21 @@ +import { Api, type ApiOptions } from "bknd"; + +type TAstro = { + request: { + url: string; + headers: Headers; + }; +}; + +export type Options = { + mode?: "static" | "dynamic"; +} & Omit & { + host?: string; + }; + +export function getApi(Astro: TAstro, options: Options = { mode: "static" }) { + return new Api({ + host: new URL(Astro.request.url).origin, + headers: options.mode === "dynamic" ? Astro.request.headers : undefined + }); +} diff --git a/app/src/adapter/astro/index.ts b/app/src/adapter/astro/index.ts new file mode 100644 index 0000000..d5010a5 --- /dev/null +++ b/app/src/adapter/astro/index.ts @@ -0,0 +1 @@ +export * from "./astro.adapter"; diff --git a/app/src/adapter/nextjs/AdminPage.tsx b/app/src/adapter/nextjs/AdminPage.tsx index 222e40f..bd0a81e 100644 --- a/app/src/adapter/nextjs/AdminPage.tsx +++ b/app/src/adapter/nextjs/AdminPage.tsx @@ -1,4 +1,5 @@ import { withApi } from "bknd/adapter/nextjs"; +import type { BkndAdminProps } from "bknd/ui"; import type { InferGetServerSidePropsType } from "next"; import dynamic from "next/dynamic"; @@ -10,15 +11,10 @@ export const getServerSideProps = withApi(async (context) => { }; }); -export function adminPage() { +export function adminPage(adminProps?: BkndAdminProps) { const Admin = dynamic(() => import("bknd/ui").then((mod) => mod.Admin), { ssr: false }); - const ClientProvider = dynamic(() => import("bknd/ui").then((mod) => mod.ClientProvider)); return (props: InferGetServerSidePropsType) => { if (typeof document === "undefined") return null; - return ( - - - - ); + return ; }; } diff --git a/app/src/adapter/remix/AdminPage.tsx b/app/src/adapter/remix/AdminPage.tsx index 6e40032..9361cc2 100644 --- a/app/src/adapter/remix/AdminPage.tsx +++ b/app/src/adapter/remix/AdminPage.tsx @@ -1,6 +1,7 @@ +import type { BkndAdminProps } from "bknd/ui"; import { Suspense, lazy, useEffect, useState } from "react"; -export function adminPage() { +export function adminPage(props?: BkndAdminProps) { const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin }))); return () => { const [loaded, setLoaded] = useState(false); @@ -12,7 +13,7 @@ export function adminPage() { return ( - + ); }; diff --git a/app/src/data/connection/LibsqlConnection.ts b/app/src/data/connection/LibsqlConnection.ts index 54202d3..9f6ebcb 100644 --- a/app/src/data/connection/LibsqlConnection.ts +++ b/app/src/data/connection/LibsqlConnection.ts @@ -1,4 +1,4 @@ -import { type Client, type InStatement, createClient } from "@libsql/client/web"; +import { type Client, type Config, type InStatement, createClient } from "@libsql/client/web"; import { LibsqlDialect } from "@libsql/kysely-libsql"; import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin, sql } from "kysely"; import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin"; @@ -8,9 +8,7 @@ import { SqliteConnection } from "./SqliteConnection"; import { SqliteIntrospector } from "./SqliteIntrospector"; export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const; -export type LibSqlCredentials = { - url: string; - authToken?: string; +export type LibSqlCredentials = Config & { protocol?: (typeof LIBSQL_PROTOCOLS)[number]; }; diff --git a/app/src/index.ts b/app/src/index.ts index 338bc7c..649bf71 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -8,5 +8,5 @@ export { type ModuleSchemas } from "modules/ModuleManager"; -export * from "./adapter"; +export type * from "./adapter"; export { Api, type ApiOptions } from "./Api"; diff --git a/app/src/ui/Admin.tsx b/app/src/ui/Admin.tsx index 2af8063..2a5aeeb 100644 --- a/app/src/ui/Admin.tsx +++ b/app/src/ui/Admin.tsx @@ -1,26 +1,36 @@ import { MantineProvider } from "@mantine/core"; import { Notifications } from "@mantine/notifications"; +import type { ModuleConfigs } from "modules"; import React from "react"; import { FlashMessage } from "ui/modules/server/FlashMessage"; -import { BkndProvider, ClientProvider, useBknd } from "./client"; +import { BkndProvider, ClientProvider, type ClientProviderProps, useBknd } from "./client"; import { createMantineTheme } from "./lib/mantine/theme"; import { BkndModalsProvider } from "./modals"; import { Routes } from "./routes"; export type BkndAdminProps = { baseUrl?: string; - withProvider?: boolean; - // @todo: add admin config override + withProvider?: boolean | ClientProviderProps; + config?: ModuleConfigs["server"]["admin"]; }; -export default function Admin({ baseUrl: baseUrlOverride, withProvider = false }: BkndAdminProps) { +export default function Admin({ + baseUrl: baseUrlOverride, + withProvider = false, + config +}: BkndAdminProps) { const Component = ( - + ); return withProvider ? ( - {Component} + + {Component} + ) : ( Component ); diff --git a/app/src/ui/client/BkndProvider.tsx b/app/src/ui/client/BkndProvider.tsx index 14e704a..b0991e7 100644 --- a/app/src/ui/client/BkndProvider.tsx +++ b/app/src/ui/client/BkndProvider.tsx @@ -14,6 +14,7 @@ type BkndContext = { requireSecrets: () => Promise; actions: ReturnType; app: AppReduced; + adminOverride?: ModuleConfigs["server"]["admin"]; }; const BkndContext = createContext(undefined!); @@ -21,8 +22,9 @@ export type { TSchemaActions }; export function BkndProvider({ includeSecrets = false, + adminOverride, children -}: { includeSecrets?: boolean; children: any }) { +}: { includeSecrets?: boolean; children: any } & Pick) { const [withSecrets, setWithSecrets] = useState(includeSecrets); const [schema, setSchema] = useState>(); @@ -64,6 +66,13 @@ export function BkndProvider({ permissions: [] } as any); + if (adminOverride) { + schema.config.server.admin = { + ...schema.config.server.admin, + ...adminOverride + }; + } + startTransition(() => { setSchema(schema); setWithSecrets(_includeSecrets); @@ -86,7 +95,7 @@ export function BkndProvider({ const actions = getSchemaActions({ client, setSchema, reloadSchema }); return ( - + {children} ); diff --git a/app/src/ui/client/ClientProvider.tsx b/app/src/ui/client/ClientProvider.tsx index 078d23a..20c2740 100644 --- a/app/src/ui/client/ClientProvider.tsx +++ b/app/src/ui/client/ClientProvider.tsx @@ -17,11 +17,13 @@ export const queryClient = new QueryClient({ } }); -export const ClientProvider = ({ - children, - baseUrl, - user -}: { children?: any; baseUrl?: string; user?: TApiUser | null }) => { +export type ClientProviderProps = { + children?: any; + baseUrl?: string; + user?: TApiUser | null | undefined; +}; + +export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps) => { const [actualBaseUrl, setActualBaseUrl] = useState(null); const winCtx = useBkndWindowContext(); diff --git a/app/src/ui/client/index.ts b/app/src/ui/client/index.ts index c5dce6e..4dbb869 100644 --- a/app/src/ui/client/index.ts +++ b/app/src/ui/client/index.ts @@ -1,4 +1,4 @@ -export { ClientProvider, useClient, useBaseUrl } from "./ClientProvider"; +export { ClientProvider, type ClientProviderProps, useClient, useBaseUrl } from "./ClientProvider"; export { BkndProvider, useBknd } from "./BkndProvider"; export { useAuth } from "./schema/auth/use-auth"; diff --git a/app/src/ui/components/display/Alert.tsx b/app/src/ui/components/display/Alert.tsx index e3b51df..ba3c4cd 100644 --- a/app/src/ui/components/display/Alert.tsx +++ b/app/src/ui/components/display/Alert.tsx @@ -1,11 +1,11 @@ -import type { ComponentPropsWithoutRef } from "react"; +import type { ComponentPropsWithoutRef, ReactNode } from "react"; import { twMerge } from "tailwind-merge"; export type AlertProps = ComponentPropsWithoutRef<"div"> & { className?: string; visible?: boolean; title?: string; - message?: string; + message?: ReactNode | string; }; const Base: React.FC = ({ visible = true, title, message, className, ...props }) => diff --git a/app/src/ui/index.ts b/app/src/ui/index.ts index 1cedbf3..ed820ab 100644 --- a/app/src/ui/index.ts +++ b/app/src/ui/index.ts @@ -1,4 +1,4 @@ -export { default as Admin } from "./Admin"; +export { default as Admin, type BkndAdminProps } from "./Admin"; export { Button } from "./components/buttons/Button"; export { Context } from "./components/Context"; export { diff --git a/app/src/ui/modules/data/components/EntityForm.tsx b/app/src/ui/modules/data/components/EntityForm.tsx index 4219547..40147b0 100644 --- a/app/src/ui/modules/data/components/EntityForm.tsx +++ b/app/src/ui/modules/data/components/EntityForm.tsx @@ -135,10 +135,10 @@ type FormInputElement = HTMLInputElement | HTMLTextAreaElement; function EntityFormField({ fieldApi, field, action, data, ...props }: EntityFormFieldProps) { const handleUpdate = useEvent((e: React.ChangeEvent | any) => { if (typeof e === "object" && "target" in e) { - console.log("handleUpdate", e.target.value); + //console.log("handleUpdate", e.target.value); fieldApi.handleChange(e.target.value); } else { - console.log("handleUpdate-", e); + //console.log("handleUpdate-", e); fieldApi.handleChange(e); } }); diff --git a/app/src/ui/routes/settings/components/Setting.tsx b/app/src/ui/routes/settings/components/Setting.tsx index 8ed58f2..1c12543 100644 --- a/app/src/ui/routes/settings/components/Setting.tsx +++ b/app/src/ui/routes/settings/components/Setting.tsx @@ -234,7 +234,7 @@ export function Setting({ - {typeof showAlert === "string" && } + {typeof showAlert !== "undefined" && }
- {/* - - - */}
); @@ -145,12 +116,7 @@ const SettingRoutesRoutes = () => { return ( <> - + diff --git a/app/src/ui/routes/settings/routes/server.settings.tsx b/app/src/ui/routes/settings/routes/server.settings.tsx new file mode 100644 index 0000000..9a1e867 --- /dev/null +++ b/app/src/ui/routes/settings/routes/server.settings.tsx @@ -0,0 +1,55 @@ +import { cloneDeep } from "lodash-es"; +import { useBknd } from "ui"; +import { Setting } from "ui/routes/settings/components/Setting"; +import { Route } from "wouter"; + +const uiSchema = { + cors: { + allow_methods: { + "ui:widget": "checkboxes" + }, + allow_headers: { + "ui:options": { + orderable: false + } + } + } +}; + +export const ServerSettings = ({ schema: _unsafe_copy, config }) => { + const { app, adminOverride } = useBknd(); + const { basepath } = app.getAdminConfig(); + const _schema = cloneDeep(_unsafe_copy); + const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/"); + + const schema = _schema; + if (adminOverride) { + schema.properties.admin.readOnly = true; + } + + return ( + + ( + { + if (adminOverride) { + return "The admin settings are read-only as they are overriden. Remaining server configuration can be edited."; + } + return; + } + }} + schema={schema} + uiSchema={uiSchema} + config={config} + prefix={`${prefix}/server`} + path={["server"]} + /> + )} + nest + /> + + ); +}; diff --git a/bun.lockb b/bun.lockb index 0d9a834..5ea65b5 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/integration/nextjs.mdx b/docs/integration/nextjs.mdx index 898568b..99b5735 100644 --- a/docs/integration/nextjs.mdx +++ b/docs/integration/nextjs.mdx @@ -38,7 +38,9 @@ import { adminPage, getServerSideProps } from "bknd/adapter/nextjs"; import "bknd/dist/styles.css"; export { getServerSideProps }; -export default adminPage(); +export default adminPage({ + config: { basepath: "/admin" } +}); ``` ## Example usage of the API in pages dir diff --git a/docs/integration/remix.mdx b/docs/integration/remix.mdx index 5e5761f..faf1bfb 100644 --- a/docs/integration/remix.mdx +++ b/docs/integration/remix.mdx @@ -81,7 +81,9 @@ Create a new splat route file at `app/routes/admin.$.tsx`: import { adminPage } from "bknd/adapter/remix"; import "bknd/dist/styles.css"; -export default adminPage(); +export default adminPage({ + config: { basepath: "/admin" } +}); ``` ## Example usage of the API diff --git a/examples/astro/.gitignore b/examples/astro/.gitignore new file mode 100644 index 0000000..a0cee65 --- /dev/null +++ b/examples/astro/.gitignore @@ -0,0 +1,24 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ \ No newline at end of file diff --git a/examples/astro/README.md b/examples/astro/README.md new file mode 100644 index 0000000..e34a99b --- /dev/null +++ b/examples/astro/README.md @@ -0,0 +1,47 @@ +# Astro Starter Kit: Minimal + +```sh +npm create astro@latest -- --template minimal +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json) + +> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +```text +/ +├── public/ +├── src/ +│ └── pages/ +│ └── index.astro +└── package.json +``` + +Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. + +There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. + +Any static assets, like images, can be placed in the `public/` directory. + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## 👀 Want to learn more? + +Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/astro/astro.config.mjs b/examples/astro/astro.config.mjs new file mode 100644 index 0000000..ba9a322 --- /dev/null +++ b/examples/astro/astro.config.mjs @@ -0,0 +1,10 @@ +// @ts-check +import { defineConfig } from "astro/config"; + +import react from "@astrojs/react"; + +// https://astro.build/config +export default defineConfig({ + output: "hybrid", + integrations: [react()] +}); diff --git a/examples/astro/package.json b/examples/astro/package.json new file mode 100644 index 0000000..ed5f51e --- /dev/null +++ b/examples/astro/package.json @@ -0,0 +1,25 @@ +{ + "name": "astro", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro check && astro build", + "preview": "astro preview", + "db": "turso dev --db-file test.db", + "db:check": "sqlite3 test.db \"PRAGMA wal_checkpoint(FULL);\"", + "astro": "astro" + }, + "dependencies": { + "@astrojs/check": "^0.9.4", + "@astrojs/react": "^3.6.3", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "astro": "^4.16.16", + "bknd": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.7.2" + } +} diff --git a/examples/astro/public/favicon.svg b/examples/astro/public/favicon.svg new file mode 100644 index 0000000..f157bd1 --- /dev/null +++ b/examples/astro/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/examples/astro/src/components/Card.astro b/examples/astro/src/components/Card.astro new file mode 100644 index 0000000..b29aa31 --- /dev/null +++ b/examples/astro/src/components/Card.astro @@ -0,0 +1,73 @@ +--- +interface Props { + title: string; + body: string; + done?: boolean; +} + +const { done, title, body } = Astro.props; +--- + + + diff --git a/examples/astro/src/env.d.ts b/examples/astro/src/env.d.ts new file mode 100644 index 0000000..e16c13c --- /dev/null +++ b/examples/astro/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/astro/src/layouts/Layout.astro b/examples/astro/src/layouts/Layout.astro new file mode 100644 index 0000000..32f2cc1 --- /dev/null +++ b/examples/astro/src/layouts/Layout.astro @@ -0,0 +1,137 @@ +--- +import Card from "../components/Card.astro"; +interface Props { + title: string; +} + +const { title } = Astro.props; +const path = new URL(Astro.request.url).pathname; +const items = [ + { href: "/", text: "Home (static)" }, + { href: "/ssr", text: "SSR (with auth)" }, + { href: "/admin", text: "Admin" } +]; +--- + + + + + + + + + + {title} + + + +
+
+

bknd + Astro

+ +
+ + +
+ + + diff --git a/examples/astro/src/pages/admin/[...admin].astro b/examples/astro/src/pages/admin/[...admin].astro new file mode 100644 index 0000000..0f5d0bf --- /dev/null +++ b/examples/astro/src/pages/admin/[...admin].astro @@ -0,0 +1,21 @@ +--- +import { Admin } from "bknd/ui"; +import "bknd/dist/styles.css"; + +import { getApi } from "bknd/adapter/astro"; + +const api = getApi(Astro, { mode: "dynamic" }); +const user = api.getUser(); + +export const prerender = false; +--- + + + + + + \ No newline at end of file diff --git a/examples/astro/src/pages/api/[...api].ts b/examples/astro/src/pages/api/[...api].ts new file mode 100644 index 0000000..5baa0fa --- /dev/null +++ b/examples/astro/src/pages/api/[...api].ts @@ -0,0 +1,21 @@ +import type { APIRoute } from "astro"; +import { App } from "bknd"; + +export const prerender = false; + +let app: App; +export const ALL: APIRoute = async ({ request }) => { + if (!app) { + app = App.create({ + connection: { + type: "libsql", + config: { + url: "http://127.0.0.1:8080" + } + } + }); + + await app.build(); + } + return app.fetch(request); +}; diff --git a/examples/astro/src/pages/index.astro b/examples/astro/src/pages/index.astro new file mode 100644 index 0000000..4c2f31b --- /dev/null +++ b/examples/astro/src/pages/index.astro @@ -0,0 +1,29 @@ +--- +import { getApi } from "bknd/adapter/astro"; +import Card from "../components/Card.astro"; +import Layout from "../layouts/Layout.astro"; +const api = getApi(Astro); +const { data } = await api.data.readMany("todos"); +--- + + +

Static Rendering

+ +
+ + diff --git a/examples/astro/src/pages/ssr.astro b/examples/astro/src/pages/ssr.astro new file mode 100644 index 0000000..eb3a8aa --- /dev/null +++ b/examples/astro/src/pages/ssr.astro @@ -0,0 +1,35 @@ +--- +import { getApi } from "bknd/adapter/astro"; +import Card from "../components/Card.astro"; +import Layout from "../layouts/Layout.astro"; +const api = getApi(Astro, { mode: "dynamic" }); +const { data } = await api.data.readMany("todos"); +const user = api.getUser(); + +export const prerender = false; +--- + + +

Server Side Rendering

+ +
+ {user ?

Logged in as {user?.email}. Logout

:

Not authenticated. Sign in

} +
+
+ + diff --git a/examples/astro/test.db b/examples/astro/test.db new file mode 100644 index 0000000..a614373 Binary files /dev/null and b/examples/astro/test.db differ diff --git a/examples/astro/tsconfig.json b/examples/astro/tsconfig.json new file mode 100644 index 0000000..bcbf8b5 --- /dev/null +++ b/examples/astro/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/strict" +} diff --git a/examples/nextjs/src/pages/admin/[[...admin]].tsx b/examples/nextjs/src/pages/admin/[[...admin]].tsx index 58b4991..bd95826 100644 --- a/examples/nextjs/src/pages/admin/[[...admin]].tsx +++ b/examples/nextjs/src/pages/admin/[[...admin]].tsx @@ -2,4 +2,8 @@ import { adminPage, getServerSideProps } from "bknd/adapter/nextjs"; import "bknd/dist/styles.css"; export { getServerSideProps }; -export default adminPage(); +export default adminPage({ + config: { + basepath: "/admin" + } +}); diff --git a/examples/nextjs/test.db b/examples/nextjs/test.db index e2b5b06..de29870 100644 Binary files a/examples/nextjs/test.db and b/examples/nextjs/test.db differ diff --git a/examples/remix/app/routes/admin.$.tsx b/examples/remix/app/routes/admin.$.tsx index 0207428..c9d725d 100644 --- a/examples/remix/app/routes/admin.$.tsx +++ b/examples/remix/app/routes/admin.$.tsx @@ -1,4 +1,8 @@ import { adminPage } from "bknd/adapter/remix"; import "bknd/dist/styles.css"; -export default adminPage(); +export default adminPage({ + config: { + basepath: "/admin" + } +});