From 6f776aeff7f4a05d1dae5b0ffdd8d9160e4f1120 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 20 Feb 2025 10:02:44 +0100 Subject: [PATCH] optimized local api instantiation to prepare for nextjs app router --- app/build.ts | 2 +- app/src/App.ts | 13 ++-- app/src/adapter/nextjs/nextjs.adapter.ts | 65 ++++++++------------ app/src/adapter/remix/remix.adapter.ts | 7 ++- app/src/index.ts | 3 +- examples/nextjs/src/bknd.ts | 68 +++++++++++++++++++++ examples/nextjs/src/pages/api/[...route].ts | 58 ++---------------- examples/nextjs/src/pages/index.tsx | 11 ++-- examples/remix/app/bknd.ts | 18 ++---- 9 files changed, 123 insertions(+), 122 deletions(-) create mode 100644 examples/nextjs/src/bknd.ts diff --git a/app/build.ts b/app/build.ts index ba14bc5..bbd4631 100644 --- a/app/build.ts +++ b/app/build.ts @@ -56,7 +56,7 @@ async function buildApi() { 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", "bknd/client"], + external: ["bun:test", "@libsql/client"], metafile: true, platform: "browser", format: ["esm"], diff --git a/app/src/App.ts b/app/src/App.ts index 06917d0..b83ad77 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -1,5 +1,5 @@ +import { Api, type ApiOptions } from "Api"; import type { CreateUserPayload } from "auth/AppAuth"; -import { Api, type ApiOptions } from "bknd/client"; import { $console } from "core"; import { Event } from "core/events"; import { Connection, type LibSqlCredentials, LibsqlConnection } from "data"; @@ -48,6 +48,7 @@ export type CreateAppConfig = { }; export type AppConfig = InitialModuleConfigs; +export type LocalApiOptions = Request | ApiOptions; export class App { modules: ModuleManager; @@ -180,13 +181,15 @@ export class App { return this.module.auth.createUser(p); } - getApi(options: Request | ApiOptions = {}) { + async getApi(options?: LocalApiOptions) { const fetcher = this.server.request as typeof fetch; - if (options instanceof Request) { - return new Api({ request: options, headers: options.headers, fetcher }); + if (options && options instanceof Request) { + const api = new Api({ request: options, headers: options.headers, fetcher }); + await api.verifyAuth(); + return api; } - return new Api({ host: "http://localhost", ...options, fetcher }); + return new Api({ host: "http://localhost", ...(options ?? {}), fetcher }); } } diff --git a/app/src/adapter/nextjs/nextjs.adapter.ts b/app/src/adapter/nextjs/nextjs.adapter.ts index adaf853..0202c08 100644 --- a/app/src/adapter/nextjs/nextjs.adapter.ts +++ b/app/src/adapter/nextjs/nextjs.adapter.ts @@ -1,49 +1,35 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; -import { nodeRequestToRequest } from "adapter/utils"; import type { App } from "bknd"; import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter"; -import { Api } from "bknd/client"; export type NextjsBkndConfig = FrameworkBkndConfig & { - cleanSearch?: string[]; + cleanRequest?: { searchParams?: string[] }; }; -type GetServerSidePropsContext = { - req: IncomingMessage; - res: ServerResponse; - params?: Params; - query: any; - preview?: boolean; - previewData?: any; - draftMode?: boolean; - resolvedUrl: string; - locale?: string; - locales?: string[]; - defaultLocale?: string; -}; +let app: App; +let building: boolean = false; -export function createApi({ req }: GetServerSidePropsContext) { - const request = nodeRequestToRequest(req); - return new Api({ - host: new URL(request.url).origin, - headers: request.headers - }); +export async function getApp(config: NextjsBkndConfig) { + if (building) { + while (building) { + await new Promise((resolve) => setTimeout(resolve, 5)); + } + if (app) return app; + } + + building = true; + if (!app) { + app = await createFrameworkApp(config); + await app.build(); + } + building = false; + return app; } -export function withApi(handler: (ctx: GetServerSidePropsContext & { api: Api }) => T) { - return async (ctx: GetServerSidePropsContext & { api: Api }) => { - const api = createApi(ctx); - await api.verifyAuth(); - return handler({ ...ctx, api }); - }; -} +function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequest"]) { + if (!cleanRequest) return req; -function getCleanRequest( - req: Request, - { cleanSearch = ["route"] }: Pick -) { const url = new URL(req.url); - cleanSearch?.forEach((k) => url.searchParams.delete(k)); + cleanRequest?.searchParams?.forEach((k) => url.searchParams.delete(k)); return new Request(url.toString(), { method: req.method, @@ -52,13 +38,12 @@ function getCleanRequest( }); } -let app: App; -export function serve({ cleanSearch, ...config }: NextjsBkndConfig = {}) { +export function serve({ cleanRequest, ...config }: NextjsBkndConfig = {}) { return async (req: Request) => { if (!app) { - app = await createFrameworkApp(config); + app = await getApp(config); } - const request = getCleanRequest(req, { cleanSearch }); - return app.fetch(request, process.env); + const request = getCleanRequest(req, cleanRequest); + return app.fetch(request); }; } diff --git a/app/src/adapter/remix/remix.adapter.ts b/app/src/adapter/remix/remix.adapter.ts index 38b81e8..0a3a4ea 100644 --- a/app/src/adapter/remix/remix.adapter.ts +++ b/app/src/adapter/remix/remix.adapter.ts @@ -10,7 +10,10 @@ type RemixContext = { let app: App; let building: boolean = false; -export async function getApp(config: RemixBkndConfig, args?: RemixContext) { +export async function getApp( + config: RemixBkndConfig, + args?: Args +) { if (building) { while (building) { await new Promise((resolve) => setTimeout(resolve, 5)); @@ -31,7 +34,7 @@ export function serve( config: RemixBkndConfig = {} ) { return async (args: Args) => { - app = await createFrameworkApp(config, args); + app = await getApp(config, args); return app.fetch(args.request); }; } diff --git a/app/src/index.ts b/app/src/index.ts index 42ab9f8..770a4c8 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -4,7 +4,8 @@ export { AppEvents, type AppConfig, type CreateAppConfig, - type AppPlugin + type AppPlugin, + type LocalApiOptions } from "./App"; export { diff --git a/examples/nextjs/src/bknd.ts b/examples/nextjs/src/bknd.ts new file mode 100644 index 0000000..7807e2f --- /dev/null +++ b/examples/nextjs/src/bknd.ts @@ -0,0 +1,68 @@ +import { App, type LocalApiOptions } from "bknd"; +import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs"; +import { boolean, em, entity, text } from "bknd/data"; +import { secureRandomString } from "bknd/utils"; + +// the em() function makes it easy to create an initial schema +const schema = em({ + todos: entity("todos", { + title: text(), + done: boolean() + }) +}); + +// register your schema to get automatic type completion +type Database = (typeof schema)["DB"]; +declare module "bknd/core" { + interface DB extends Database {} +} + +export const config = { + // we can use any libsql config, and if omitted, uses in-memory + connection: { + url: "http://localhost:8080" + }, + // an initial config is only applied if the database is empty + initialConfig: { + data: schema.toJSON(), + // we're enabling auth ... + auth: { + enabled: true, + jwt: { + secret: secureRandomString(64) + } + } + }, + options: { + // the seed option is only executed if the database was empty + seed: async (ctx) => { + await ctx.em.mutator("todos").insertMany([ + { title: "Learn bknd", done: true }, + { title: "Build something cool", done: false } + ]); + } + }, + // here we can hook into the app lifecycle events ... + beforeBuild: async (app) => { + app.emgr.onEvent( + App.Events.AppFirstBoot, + async () => { + // ... to create an initial user + await app.module.auth.createUser({ + email: "ds@bknd.io", + password: "12345678" + }); + }, + "sync" + ); + } +} as const satisfies NextjsBkndConfig; + +export async function getApp() { + return await getBkndApp(config); +} + +export async function getApi(options?: LocalApiOptions) { + const app = await getApp(); + return await app.getApi(options); +} diff --git a/examples/nextjs/src/pages/api/[...route].ts b/examples/nextjs/src/pages/api/[...route].ts index 6c3c5c8..ca039ed 100644 --- a/examples/nextjs/src/pages/api/[...route].ts +++ b/examples/nextjs/src/pages/api/[...route].ts @@ -1,7 +1,5 @@ -import { App } from "bknd"; +import { config as bkndConfig } from "@/bknd"; import { serve } from "bknd/adapter/nextjs"; -import { boolean, em, entity, text } from "bknd/data"; -import { secureRandomString } from "bknd/utils"; export const config = { runtime: "edge", @@ -12,57 +10,9 @@ export const config = { unstable_allowDynamic: ["**/*.js"] }; -// the em() function makes it easy to create an initial schema -const schema = em({ - todos: entity("todos", { - title: text(), - done: boolean() - }) -}); - -// register your schema to get automatic type completion -type Database = (typeof schema)["DB"]; -declare module "bknd/core" { - interface DB extends Database {} -} - export default serve({ - // we can use any libsql config, and if omitted, uses in-memory - connection: { - url: "http://localhost:8080" + cleanRequest: { + searchParams: ["route"] }, - // an initial config is only applied if the database is empty - initialConfig: { - data: schema.toJSON(), - // we're enabling auth ... - auth: { - enabled: true, - jwt: { - secret: secureRandomString(64) - } - } - }, - options: { - // the seed option is only executed if the database was empty - seed: async (ctx) => { - await ctx.em.mutator("todos").insertMany([ - { title: "Learn bknd", done: true }, - { title: "Build something cool", done: false } - ]); - } - }, - // here we can hook into the app lifecycle events ... - beforeBuild: async (app) => { - app.emgr.onEvent( - App.Events.AppFirstBoot, - async () => { - // ... to create an initial user - await app.module.auth.createUser({ - email: "ds@bknd.io", - password: "12345678" - }); - }, - "sync" - ); - } + ...bkndConfig }); diff --git a/examples/nextjs/src/pages/index.tsx b/examples/nextjs/src/pages/index.tsx index 53e81f0..d437e05 100644 --- a/examples/nextjs/src/pages/index.tsx +++ b/examples/nextjs/src/pages/index.tsx @@ -1,12 +1,13 @@ -import { withApi } from "bknd/adapter/nextjs"; +import { getApi } from "@/bknd"; import type { InferGetServerSidePropsType } from "next"; -export const getServerSideProps = withApi(async (context) => { - const { data = [] } = await context.api.data.readMany("todos"); - const user = context.api.getUser(); +export const getServerSideProps = async () => { + const api = await getApi(); + const { data = [] } = await api.data.readMany("todos"); + const user = api.getUser(); return { props: { data, user } }; -}); +}; export default function Home({ data, diff --git a/examples/remix/app/bknd.ts b/examples/remix/app/bknd.ts index 3a71f75..f0a8ed6 100644 --- a/examples/remix/app/bknd.ts +++ b/examples/remix/app/bknd.ts @@ -1,4 +1,4 @@ -import { App } from "bknd"; +import { App, type LocalApiOptions } from "bknd"; import { registerLocalMediaAdapter } from "bknd/adapter/node"; import { type RemixBkndConfig, getApp as getBkndApp } from "bknd/adapter/remix"; import { boolean, em, entity, text } from "bknd/data"; @@ -76,17 +76,7 @@ export async function getApp(args?: { request: Request }) { return await getBkndApp(config, args); } -/** - * If args are given, it will use authentication details from the request - * @param args - */ -export async function getApi(args?: { request: Request }) { - const app = await getApp(args); - if (args) { - const api = app.getApi(args.request); - await api.verifyAuth(); - return api; - } - - return app.getApi(); +export async function getApi(options?: LocalApiOptions) { + const app = await getApp(); + return await app.getApi(options); }