mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
Merge pull request #88 from bknd-io/refactor/improve-remix-example
refactor/improve-remix-example
This commit is contained in:
@@ -56,7 +56,7 @@ async function buildApi() {
|
|||||||
watch,
|
watch,
|
||||||
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: ["bun:test", "@libsql/client"],
|
external: ["bun:test", "@libsql/client", "bknd/client"],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
|
|||||||
@@ -49,13 +49,14 @@
|
|||||||
"hono": "^4.6.12",
|
"hono": "^4.6.12",
|
||||||
"json-schema-form-react": "^0.0.2",
|
"json-schema-form-react": "^0.0.2",
|
||||||
"json-schema-library": "^10.0.0-rc7",
|
"json-schema-library": "^10.0.0-rc7",
|
||||||
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"kysely": "^0.27.4",
|
"kysely": "^0.27.4",
|
||||||
"liquidjs": "^10.15.0",
|
"liquidjs": "^10.15.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"object-path-immutable": "^4.1.2",
|
"object-path-immutable": "^4.1.2",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
"radix-ui": "^1.1.2",
|
"radix-ui": "^1.1.2",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
|
||||||
"swr": "^2.2.5"
|
"swr": "^2.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
import type { CreateUserPayload } from "auth/AppAuth";
|
||||||
|
import { Api, type ApiOptions } from "bknd/client";
|
||||||
|
import { $console } from "core";
|
||||||
import { Event } from "core/events";
|
import { Event } from "core/events";
|
||||||
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
@@ -69,17 +71,17 @@ export class App {
|
|||||||
// respond to events, such as "onUpdated".
|
// respond to events, such as "onUpdated".
|
||||||
// this is important if multiple changes are done, and then build() is called manually
|
// this is important if multiple changes are done, and then build() is called manually
|
||||||
if (!this.emgr.enabled) {
|
if (!this.emgr.enabled) {
|
||||||
console.warn("App config updated, but event manager is disabled, skip.");
|
$console.warn("App config updated, but event manager is disabled, skip.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("App config updated", key);
|
$console.log("App config updated", key);
|
||||||
// @todo: potentially double syncing
|
// @todo: potentially double syncing
|
||||||
await this.build({ sync: true });
|
await this.build({ sync: true });
|
||||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||||
},
|
},
|
||||||
onFirstBoot: async () => {
|
onFirstBoot: async () => {
|
||||||
console.log("App first boot");
|
$console.log("App first boot");
|
||||||
this.trigger_first_boot = true;
|
this.trigger_first_boot = true;
|
||||||
},
|
},
|
||||||
onServerInit: async (server) => {
|
onServerInit: async (server) => {
|
||||||
@@ -177,6 +179,15 @@ export class App {
|
|||||||
async createUser(p: CreateUserPayload) {
|
async createUser(p: CreateUserPayload) {
|
||||||
return this.module.auth.createUser(p);
|
return this.module.auth.createUser(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getApi(options: Request | ApiOptions = {}) {
|
||||||
|
const fetcher = this.server.request as typeof fetch;
|
||||||
|
if (options instanceof Request) {
|
||||||
|
return new Api({ request: options, headers: options.headers, fetcher });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Api({ host: "http://localhost", ...options, fetcher });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApp(config: CreateAppConfig = {}) {
|
export function createApp(config: CreateAppConfig = {}) {
|
||||||
@@ -187,7 +198,7 @@ export function createApp(config: CreateAppConfig = {}) {
|
|||||||
connection = config.connection;
|
connection = config.connection;
|
||||||
} else if (typeof config.connection === "object") {
|
} else if (typeof config.connection === "object") {
|
||||||
if ("type" in config.connection) {
|
if ("type" in config.connection) {
|
||||||
console.warn(
|
$console.warn(
|
||||||
"Using deprecated connection type 'libsql', use the 'config' object directly."
|
"Using deprecated connection type 'libsql', use the 'config' object directly."
|
||||||
);
|
);
|
||||||
connection = new LibsqlConnection(config.connection.config);
|
connection = new LibsqlConnection(config.connection.config);
|
||||||
@@ -196,10 +207,10 @@ export function createApp(config: CreateAppConfig = {}) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
connection = new LibsqlConnection({ url: ":memory:" });
|
connection = new LibsqlConnection({ url: ":memory:" });
|
||||||
console.warn("No connection provided, using in-memory database");
|
$console.warn("No connection provided, using in-memory database");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not create connection", e);
|
$console.error("Could not create connection", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
|
||||||
import { Api } from "bknd/client";
|
|
||||||
|
|
||||||
export type RemixBkndConfig<Args = RemixContext> = FrameworkBkndConfig<Args>;
|
export type RemixBkndConfig<Args = RemixContext> = FrameworkBkndConfig<Args>;
|
||||||
|
|
||||||
@@ -9,29 +8,30 @@ type RemixContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let app: App;
|
let app: App;
|
||||||
|
let building: boolean = false;
|
||||||
|
|
||||||
|
export async function getApp(config: RemixBkndConfig, args?: RemixContext) {
|
||||||
|
if (building) {
|
||||||
|
while (building) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
}
|
||||||
|
if (app) return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
building = true;
|
||||||
|
if (!app) {
|
||||||
|
app = await createFrameworkApp(config, args);
|
||||||
|
await app.build();
|
||||||
|
}
|
||||||
|
building = false;
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
export function serve<Args extends RemixContext = RemixContext>(
|
export function serve<Args extends RemixContext = RemixContext>(
|
||||||
config: RemixBkndConfig<Args> = {}
|
config: RemixBkndConfig<Args> = {}
|
||||||
) {
|
) {
|
||||||
return async (args: Args) => {
|
return async (args: Args) => {
|
||||||
if (!app) {
|
app = await createFrameworkApp(config, args);
|
||||||
app = await createFrameworkApp(config, args);
|
|
||||||
}
|
|
||||||
return app.fetch(args.request);
|
return app.fetch(args.request);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withApi<Args extends { request: Request; context: { api: Api } }, R>(
|
|
||||||
handler: (args: Args, api: Api) => Promise<R>
|
|
||||||
) {
|
|
||||||
return async (args: Args) => {
|
|
||||||
if (!args.context.api) {
|
|
||||||
args.context.api = new Api({
|
|
||||||
host: new URL(args.request.url).origin,
|
|
||||||
headers: args.request.headers
|
|
||||||
});
|
|
||||||
await args.context.api.verifyAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler(args, args.context.api);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { CliCommand } from "cli/types";
|
|||||||
import { typewriter, wait } from "cli/utils/cli";
|
import { typewriter, wait } from "cli/utils/cli";
|
||||||
import { execAsync, getVersion } from "cli/utils/sys";
|
import { execAsync, getVersion } from "cli/utils/sys";
|
||||||
import { Option } from "commander";
|
import { Option } from "commander";
|
||||||
|
import { colorizeConsole } from "core";
|
||||||
import color from "picocolors";
|
import color from "picocolors";
|
||||||
import { overridePackageJson, updateBkndPackages } from "./npm";
|
import { overridePackageJson, updateBkndPackages } from "./npm";
|
||||||
import { type Template, templates } from "./templates";
|
import { type Template, templates } from "./templates";
|
||||||
@@ -48,6 +49,7 @@ function errorOutro() {
|
|||||||
|
|
||||||
async function action(options: { template?: string; dir?: string; integration?: string }) {
|
async function action(options: { template?: string; dir?: string; integration?: string }) {
|
||||||
console.log("");
|
console.log("");
|
||||||
|
colorizeConsole(console);
|
||||||
|
|
||||||
const downloadOpts = {
|
const downloadOpts = {
|
||||||
dir: options.dir || "./",
|
dir: options.dir || "./",
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const subjects = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function action(subject: string) {
|
async function action(subject: string) {
|
||||||
console.log("debug", { subject });
|
|
||||||
if (subject in subjects) {
|
if (subject in subjects) {
|
||||||
await subjects[subject]();
|
await subjects[subject]();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import type { Config } from "@libsql/client/node";
|
|||||||
import { App, type CreateAppConfig } from "App";
|
import { App, type CreateAppConfig } from "App";
|
||||||
import { StorageLocalAdapter } from "adapter/node";
|
import { StorageLocalAdapter } from "adapter/node";
|
||||||
import type { CliBkndConfig, CliCommand } from "cli/types";
|
import type { CliBkndConfig, CliCommand } from "cli/types";
|
||||||
import { replaceConsole } from "cli/utils/cli";
|
|
||||||
import { Option } from "commander";
|
import { Option } from "commander";
|
||||||
import { config } from "core";
|
import { colorizeConsole, config } from "core";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { registries } from "modules/registries";
|
import { registries } from "modules/registries";
|
||||||
import c from "picocolors";
|
import c from "picocolors";
|
||||||
@@ -112,7 +111,7 @@ async function action(options: {
|
|||||||
dbToken?: string;
|
dbToken?: string;
|
||||||
server: Platform;
|
server: Platform;
|
||||||
}) {
|
}) {
|
||||||
replaceConsole();
|
colorizeConsole(console);
|
||||||
const configFilePath = await getConfigPath(options.config);
|
const configFilePath = await getConfigPath(options.config);
|
||||||
|
|
||||||
let app: App | undefined = undefined;
|
let app: App | undefined = undefined;
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { isDebug } from "core";
|
|
||||||
import c from "picocolors";
|
|
||||||
import type { Formatter } from "picocolors/types";
|
|
||||||
const _SPEEDUP = process.env.LOCAL;
|
const _SPEEDUP = process.env.LOCAL;
|
||||||
|
|
||||||
const DEFAULT_WAIT = _SPEEDUP ? 0 : 250;
|
const DEFAULT_WAIT = _SPEEDUP ? 0 : 250;
|
||||||
@@ -57,31 +54,3 @@ export async function* typewriter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ifString(args: any[], c: Formatter) {
|
|
||||||
return args.map((a) => (typeof a === "string" ? c(a) : a));
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalConsole = {
|
|
||||||
log: console.log,
|
|
||||||
info: console.info,
|
|
||||||
debug: console.debug,
|
|
||||||
warn: console.warn,
|
|
||||||
error: console.error
|
|
||||||
};
|
|
||||||
|
|
||||||
export const $console = {
|
|
||||||
log: (...args: any[]) => originalConsole.info(c.gray("[LOG] "), ...ifString(args, c.dim)),
|
|
||||||
info: (...args: any[]) => originalConsole.info(c.cyan("[INFO] "), ...args),
|
|
||||||
debug: (...args: any[]) => isDebug() && originalConsole.info(c.yellow("[DEBUG]"), ...args),
|
|
||||||
warn: (...args: any[]) => originalConsole.info(c.yellow("[WARN] "), ...ifString(args, c.yellow)),
|
|
||||||
error: (...args: any[]) => originalConsole.info(c.red("[ERROR]"), ...ifString(args, c.red))
|
|
||||||
};
|
|
||||||
|
|
||||||
export function replaceConsole() {
|
|
||||||
console.log = $console.log;
|
|
||||||
console.info = $console.info;
|
|
||||||
console.debug = $console.debug;
|
|
||||||
console.warn = $console.warn;
|
|
||||||
console.error = $console.error;
|
|
||||||
}
|
|
||||||
|
|||||||
105
app/src/core/console.ts
Normal file
105
app/src/core/console.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import colors from "picocolors";
|
||||||
|
|
||||||
|
function hasColors() {
|
||||||
|
try {
|
||||||
|
// biome-ignore lint/style/useSingleVarDeclarator: <explanation>
|
||||||
|
const p = process || {},
|
||||||
|
argv = p.argv || [],
|
||||||
|
env = p.env || {};
|
||||||
|
return (
|
||||||
|
!(!!env.NO_COLOR || argv.includes("--no-color")) &&
|
||||||
|
// biome-ignore lint/complexity/useOptionalChain: <explanation>
|
||||||
|
(!!env.FORCE_COLOR ||
|
||||||
|
argv.includes("--color") ||
|
||||||
|
p.platform === "win32" ||
|
||||||
|
((p.stdout || {}).isTTY && env.TERM !== "dumb") ||
|
||||||
|
!!env.CI)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalConsoles = {
|
||||||
|
error: console.error,
|
||||||
|
warn: console.warn,
|
||||||
|
info: console.info,
|
||||||
|
log: console.log,
|
||||||
|
debug: console.debug
|
||||||
|
} as typeof console;
|
||||||
|
|
||||||
|
function __tty(type: any, args: any[]) {
|
||||||
|
const has = hasColors();
|
||||||
|
const styles = {
|
||||||
|
error: {
|
||||||
|
prefix: colors.red,
|
||||||
|
args: colors.red
|
||||||
|
},
|
||||||
|
warn: {
|
||||||
|
prefix: colors.yellow,
|
||||||
|
args: colors.yellow
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
prefix: colors.cyan
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
prefix: colors.gray
|
||||||
|
},
|
||||||
|
debug: {
|
||||||
|
prefix: colors.yellow
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
const prefix = styles[type].prefix(
|
||||||
|
`[${type.toUpperCase()}]${has ? " ".repeat(5 - type.length) : ""}`
|
||||||
|
);
|
||||||
|
const _args = args.map((a) =>
|
||||||
|
"args" in styles[type] && has && typeof a === "string" ? styles[type].args(a) : a
|
||||||
|
);
|
||||||
|
return originalConsoles[type](prefix, ..._args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TConsoleSeverity = keyof typeof originalConsoles;
|
||||||
|
const severities = Object.keys(originalConsoles) as TConsoleSeverity[];
|
||||||
|
|
||||||
|
let enabled = [...severities];
|
||||||
|
|
||||||
|
export function disableConsole(severities: TConsoleSeverity[] = enabled) {
|
||||||
|
enabled = enabled.filter((s) => !severities.includes(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableConsole() {
|
||||||
|
enabled = [...severities];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $console = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (_, prop) => {
|
||||||
|
if (prop in originalConsoles && enabled.includes(prop as TConsoleSeverity)) {
|
||||||
|
return (...args: any[]) => __tty(prop, args);
|
||||||
|
}
|
||||||
|
return () => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) as typeof console;
|
||||||
|
|
||||||
|
export async function withDisabledConsole<R>(
|
||||||
|
fn: () => Promise<R>,
|
||||||
|
sev?: TConsoleSeverity[]
|
||||||
|
): Promise<R> {
|
||||||
|
disableConsole(sev);
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
enableConsole();
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
enableConsole();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorizeConsole(con: typeof console) {
|
||||||
|
for (const [key] of Object.entries(originalConsoles)) {
|
||||||
|
con[key] = $console[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ export {
|
|||||||
} from "./object/query/query";
|
} from "./object/query/query";
|
||||||
export { Registry, type Constructor } from "./registry/Registry";
|
export { Registry, type Constructor } from "./registry/Registry";
|
||||||
|
|
||||||
|
export * from "./console";
|
||||||
|
|
||||||
// compatibility
|
// compatibility
|
||||||
export type Middleware = MiddlewareHandler<any, any, any>;
|
export type Middleware = MiddlewareHandler<any, any, any>;
|
||||||
export interface ClassController {
|
export interface ClassController {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class LibsqlConnection extends SqliteConnection {
|
|||||||
constructor(clientOrCredentials: Client | LibSqlCredentials) {
|
constructor(clientOrCredentials: Client | LibSqlCredentials) {
|
||||||
const plugins = [new FilterNumericKeysPlugin(), new ParseJSONResultsPlugin()];
|
const plugins = [new FilterNumericKeysPlugin(), new ParseJSONResultsPlugin()];
|
||||||
let client: Client;
|
let client: Client;
|
||||||
if ("url" in clientOrCredentials) {
|
if (clientOrCredentials && "url" in clientOrCredentials) {
|
||||||
let { url, authToken, protocol } = clientOrCredentials;
|
let { url, authToken, protocol } = clientOrCredentials;
|
||||||
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
|
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
|
||||||
console.log("changing protocol to", protocol);
|
console.log("changing protocol to", protocol);
|
||||||
@@ -36,11 +36,8 @@ export class LibsqlConnection extends SqliteConnection {
|
|||||||
url = `${protocol}://${rest}`;
|
url = `${protocol}://${rest}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("using", url, { protocol });
|
|
||||||
|
|
||||||
client = createClient({ url, authToken });
|
client = createClient({ url, authToken });
|
||||||
} else {
|
} else {
|
||||||
//console.log("-- client provided");
|
|
||||||
client = clientOrCredentials;
|
client = clientOrCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +45,6 @@ export class LibsqlConnection extends SqliteConnection {
|
|||||||
// @ts-expect-error libsql has type issues
|
// @ts-expect-error libsql has type issues
|
||||||
dialect: new CustomLibsqlDialect({ client }),
|
dialect: new CustomLibsqlDialect({ client }),
|
||||||
plugins
|
plugins
|
||||||
//log: ["query"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
super(kysely, {}, plugins);
|
super(kysely, {}, plugins);
|
||||||
@@ -90,7 +86,6 @@ export class LibsqlConnection extends SqliteConnection {
|
|||||||
const rows = await kyselyPlugins.transformResultRows(r.rows);
|
const rows = await kyselyPlugins.transformResultRows(r.rows);
|
||||||
data.push(rows);
|
data.push(rows);
|
||||||
}
|
}
|
||||||
//console.log("data", data);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { DB as DefaultDB, PrimaryFieldType } from "core";
|
import type { DB as DefaultDB, PrimaryFieldType } from "core";
|
||||||
|
import { $console } from "core";
|
||||||
import { type EmitsEvents, EventManager } from "core/events";
|
import { type EmitsEvents, EventManager } from "core/events";
|
||||||
import { type SelectQueryBuilder, sql } from "kysely";
|
import { type SelectQueryBuilder, sql } from "kysely";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
@@ -161,7 +162,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
protected async performQuery(qb: RepositoryQB): Promise<RepositoryResponse> {
|
||||||
const entity = this.entity;
|
const entity = this.entity;
|
||||||
const compiled = qb.compile();
|
const compiled = qb.compile();
|
||||||
//console.log("performQuery", compiled.sql, compiled.parameters);
|
//$console.log("performQuery", compiled.sql, compiled.parameters);
|
||||||
|
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
|
const selector = (as = "count") => this.conn.fn.countAll<number>().as(as);
|
||||||
@@ -180,7 +181,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
totalQuery,
|
totalQuery,
|
||||||
qb
|
qb
|
||||||
]);
|
]);
|
||||||
//console.log("result", { _count, _total });
|
//$console.log("result", { _count, _total });
|
||||||
|
|
||||||
const time = Number.parseFloat((performance.now() - start).toFixed(2));
|
const time = Number.parseFloat((performance.now() - start).toFixed(2));
|
||||||
const data = this.em.hydrate(entity.name, result);
|
const data = this.em.hydrate(entity.name, result);
|
||||||
@@ -201,7 +202,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
console.error("[ERROR] Repository.performQuery", e.message);
|
$console.error("[ERROR] Repository.performQuery", e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
@@ -254,7 +255,7 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
...config?.defaults
|
...config?.defaults
|
||||||
};
|
};
|
||||||
|
|
||||||
/*console.log("build query options", {
|
/*$console.log("build query options", {
|
||||||
entity: entity.name,
|
entity: entity.name,
|
||||||
options,
|
options,
|
||||||
config
|
config
|
||||||
@@ -426,9 +427,9 @@ export class Repository<TBD extends object = DefaultDB, TB extends keyof TBD = a
|
|||||||
qb = qb.limit(1);
|
qb = qb.limit(1);
|
||||||
|
|
||||||
const compiled = qb.compile();
|
const compiled = qb.compile();
|
||||||
//console.log("exists query", compiled.sql, compiled.parameters);
|
//$console.log("exists query", compiled.sql, compiled.parameters);
|
||||||
const result = await qb.execute();
|
const result = await qb.execute();
|
||||||
//console.log("result", result);
|
//$console.log("result", result);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sql: compiled.sql,
|
sql: compiled.sql,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Guard } from "auth";
|
import { Guard } from "auth";
|
||||||
import { BkndError, DebugLogger } from "core";
|
import { BkndError, DebugLogger, withDisabledConsole } from "core";
|
||||||
import { EventManager } from "core/events";
|
import { EventManager } from "core/events";
|
||||||
import { clone, diff } from "core/object/diff";
|
import { clone, diff } from "core/object/diff";
|
||||||
import {
|
import {
|
||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
mark,
|
mark,
|
||||||
objectEach,
|
objectEach,
|
||||||
stripMark,
|
stripMark,
|
||||||
transformObject,
|
transformObject
|
||||||
withDisabledConsole
|
|
||||||
} from "core/utils";
|
} from "core/utils";
|
||||||
import {
|
import {
|
||||||
type Connection,
|
type Connection,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type AdminControllerOptions = {
|
|||||||
assets_path?: string;
|
assets_path?: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
forceDev?: boolean | { mainPath: string };
|
forceDev?: boolean | { mainPath: string };
|
||||||
|
debug_rerenders?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AdminController extends Controller {
|
export class AdminController extends Controller {
|
||||||
@@ -163,17 +164,13 @@ export class AdminController extends Controller {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isProd) {
|
if (isProd) {
|
||||||
try {
|
// @ts-ignore
|
||||||
// @ts-ignore
|
const manifest = await import("bknd/dist/manifest.json", {
|
||||||
const manifest = await import("bknd/dist/manifest.json", {
|
assert: { type: "json" }
|
||||||
assert: { type: "json" }
|
});
|
||||||
}).then((m) => m.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;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error loading manifest", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = configs.server.admin.color_scheme ?? "light";
|
const theme = configs.server.admin.color_scheme ?? "light";
|
||||||
@@ -192,10 +189,12 @@ export class AdminController extends Controller {
|
|||||||
/>
|
/>
|
||||||
<link rel="icon" href={favicon} type="image/x-icon" />
|
<link rel="icon" href={favicon} type="image/x-icon" />
|
||||||
<title>BKND</title>
|
<title>BKND</title>
|
||||||
{/*<script
|
{this.options.debug_rerenders && (
|
||||||
crossOrigin="anonymous"
|
<script
|
||||||
src="//unpkg.com/react-scan/dist/auto.global.js"
|
crossOrigin="anonymous"
|
||||||
/>*/}
|
src="//unpkg.com/react-scan/dist/auto.global.js"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isProd ? (
|
{isProd ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<script
|
<script
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps)
|
|||||||
console.warn("wrapped many times, take from context", actualBaseUrl);
|
console.warn("wrapped many times, take from context", actualBaseUrl);
|
||||||
} else if (typeof window !== "undefined") {
|
} else if (typeof window !== "undefined") {
|
||||||
actualBaseUrl = window.location.origin;
|
actualBaseUrl = window.location.origin;
|
||||||
console.log("setting from window", actualBaseUrl);
|
//console.log("setting from window", actualBaseUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("error .....", e);
|
console.error("Error in ClientProvider", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("api init", { host: actualBaseUrl, user: user ?? winCtx.user });
|
//console.log("api init", { host: actualBaseUrl, user: user ?? winCtx.user });
|
||||||
|
|||||||
@@ -9,28 +9,52 @@ Install bknd as a dependency:
|
|||||||
<InstallBknd />
|
<InstallBknd />
|
||||||
|
|
||||||
## Serve the API
|
## Serve the API
|
||||||
Create a new api splat route file at `app/routes/api.$.ts`:
|
Since Remix doesn't support middleware yet, we need a helper file to initialize the App to import from. Create a new file at `app/bknd.ts`:
|
||||||
```ts
|
```ts app/bknd.ts
|
||||||
// app/routes/api.$.ts
|
import { type RemixBkndConfig, getApp as getBkndApp } from "bknd/adapter/remix";
|
||||||
import { serve } from "bknd/adapter/remix";
|
|
||||||
|
|
||||||
const handler = serve({
|
const config = {
|
||||||
connection: {
|
connection: {
|
||||||
url: "http://localhost:8080"
|
url: "file:data.db"
|
||||||
}
|
}
|
||||||
});
|
} as const satisfies RemixBkndConfig;
|
||||||
|
|
||||||
|
export async function getApp(args?: { request: Request }) {
|
||||||
|
return await getBkndApp(config, 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();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new api splat route file at `app/routes/api.$.ts`:
|
||||||
|
```ts app/routes/api.$.ts
|
||||||
|
import { getApp } from "~/bknd";
|
||||||
|
|
||||||
|
const handler = async (args: { request: Request }) => {
|
||||||
|
const app = await getApp(args);
|
||||||
|
return app.fetch(args.request);
|
||||||
|
};
|
||||||
|
|
||||||
export const loader = handler;
|
export const loader = handler;
|
||||||
export const action = handler;
|
export const action = handler;
|
||||||
```
|
```
|
||||||
For more information about the connection object, refer to the [Database](/usage/database) guide.
|
For more information about the connection object, refer to the [Database](/usage/database) guide.
|
||||||
|
|
||||||
Now make sure that you wrap your root layout with the `ClientProvider` so that all components
|
Now make sure that you wrap your root layout with the `ClientProvider` so that all components share the same context. Also add the user context to both the `Outlet` and the provider:
|
||||||
share the same context:
|
```tsx app/root.tsx
|
||||||
```tsx
|
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
// app/root.tsx
|
import { useLoaderData, Outlet } from "@remix-run/react";
|
||||||
import { withApi } from "bknd/adapter/remix"
|
import { ClientProvider } from "bknd/client";
|
||||||
import { type Api, ClientProvider } from "bknd/client";
|
import { getApi } from "~/bknd";
|
||||||
|
|
||||||
export function Layout(props) {
|
export function Layout(props) {
|
||||||
// nothing to change here, just for orientation
|
// nothing to change here, just for orientation
|
||||||
@@ -39,27 +63,18 @@ export function Layout(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the api to the `AppLoadContext`
|
export const loader = async (args: LoaderFunctionArgs) => {
|
||||||
// so you don't have to manually type it again
|
const api = await getApi(args);
|
||||||
declare module "@remix-run/server-runtime" {
|
|
||||||
export interface AppLoadContext {
|
|
||||||
api: Api;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// export a loader that initiates the API
|
|
||||||
// and passes it down to args.context.api
|
|
||||||
export const loader = withApi(async (args: LoaderFunctionArgs, api: Api) => {
|
|
||||||
return {
|
return {
|
||||||
user: api.getUser()
|
user: api.getUser()
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { user } = useLoaderData<typeof loader>();
|
const data = useLoaderData<typeof loader>();
|
||||||
return (
|
return (
|
||||||
<ClientProvider user={user}>
|
<ClientProvider user={data.user}>
|
||||||
<Outlet />
|
<Outlet context={data} />
|
||||||
</ClientProvider>
|
</ClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -67,24 +82,29 @@ export default function App() {
|
|||||||
|
|
||||||
## Enabling the Admin UI
|
## Enabling the Admin UI
|
||||||
Create a new splat route file at `app/routes/admin.$.tsx`:
|
Create a new splat route file at `app/routes/admin.$.tsx`:
|
||||||
```tsx
|
```tsx app/routes/admin.$.tsx
|
||||||
// app/routes/admin.$.tsx
|
|
||||||
import { adminPage } from "bknd/adapter/remix";
|
import { adminPage } from "bknd/adapter/remix";
|
||||||
import "bknd/dist/styles.css";
|
import "bknd/dist/styles.css";
|
||||||
|
|
||||||
export default adminPage({
|
export default adminPage({
|
||||||
config: { basepath: "/admin" }
|
config: {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/../",
|
||||||
|
color_scheme: "system"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example usage of the API
|
## Example usage of the API
|
||||||
Since the API has already been constructed in the root layout, you can now use it in any page:
|
Since the API has already been constructed in the root layout, you can now use it in any page:
|
||||||
```tsx
|
```tsx app/routes/_index.tsx
|
||||||
// app/routes/_index.tsx
|
|
||||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
|
||||||
import { useLoaderData } from "@remix-run/react";
|
import { useLoaderData } from "@remix-run/react";
|
||||||
|
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { getApi } from "~/bknd";
|
||||||
|
|
||||||
export const loader = async ({ context: { api } }: LoaderFunctionArgs) => {
|
export const loader = async (args: LoaderFunctionArgs) => {
|
||||||
|
// use authentication from request
|
||||||
|
const api = await getApi(args);
|
||||||
const { data } = await api.data.readMany("todos");
|
const { data } = await api.data.readMany("todos");
|
||||||
return { data, user: api.getUser() };
|
return { data, user: api.getUser() };
|
||||||
};
|
};
|
||||||
|
|||||||
92
examples/remix/app/bknd.ts
Normal file
92
examples/remix/app/bknd.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { App } 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";
|
||||||
|
import { secureRandomString } from "bknd/utils";
|
||||||
|
|
||||||
|
// since we're running in node, we can register the local media adapter
|
||||||
|
registerLocalMediaAdapter();
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
// we can use any libsql config, and if omitted, uses in-memory
|
||||||
|
connection: {
|
||||||
|
url: "file:test.db"
|
||||||
|
},
|
||||||
|
// an initial config is only applied if the database is empty
|
||||||
|
initialConfig: {
|
||||||
|
data: schema.toJSON(),
|
||||||
|
// we're enabling auth ...
|
||||||
|
auth: {
|
||||||
|
enabled: true,
|
||||||
|
jwt: {
|
||||||
|
issuer: "bknd-remix-example",
|
||||||
|
secret: secureRandomString(64)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// ... and media
|
||||||
|
media: {
|
||||||
|
enabled: true,
|
||||||
|
adapter: {
|
||||||
|
type: "local",
|
||||||
|
config: {
|
||||||
|
path: "./public"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 RemixBkndConfig;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
9
examples/remix/app/components/Check.tsx
Normal file
9
examples/remix/app/components/Check.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function Check({ checked = false }: { checked?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`aspect-square w-6 leading-none rounded-full p-px transition-colors cursor-pointer ${checked ? "bg-green-500" : "bg-white/20 hover:bg-white/40"}`}
|
||||||
|
>
|
||||||
|
<input type="checkbox" checked={checked} readOnly />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||||
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from "@remix-run/react";
|
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from "@remix-run/react";
|
||||||
import { withApi } from "bknd/adapter/remix";
|
import { ClientProvider } from "bknd/client";
|
||||||
import { type Api, ClientProvider } from "bknd/client";
|
import "./tailwind.css";
|
||||||
|
import { getApi } from "~/bknd";
|
||||||
|
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@@ -21,26 +22,18 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "@remix-run/server-runtime" {
|
export const loader = async (args: LoaderFunctionArgs) => {
|
||||||
export interface AppLoadContext {
|
const api = await getApi(args);
|
||||||
api: Api;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loader = withApi(async (args: LoaderFunctionArgs, api: Api) => {
|
|
||||||
return {
|
return {
|
||||||
user: api.getUser()
|
user: api.getUser()
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const data = useLoaderData<typeof loader>();
|
const data = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
// add user to the client provider to indicate
|
|
||||||
// that you're authed using cookie
|
|
||||||
return (
|
return (
|
||||||
<ClientProvider user={data.user}>
|
<ClientProvider user={data.user}>
|
||||||
<Outlet />
|
<Outlet context={data} />
|
||||||
</ClientProvider>
|
</ClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,151 @@
|
|||||||
import { type MetaFunction, useLoaderData } from "@remix-run/react";
|
import { type MetaFunction, useFetcher, useLoaderData, useOutletContext } from "@remix-run/react";
|
||||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { getApi } from "~/bknd";
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [{ title: "Remix & bknd" }, { name: "description", content: "Welcome to Remix & bknd!" }];
|
return [
|
||||||
|
{ title: "New bknd-Remix App" },
|
||||||
|
{ name: "description", content: "Welcome to bknd & Remix!" }
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loader = async ({ context: { api } }: LoaderFunctionArgs) => {
|
export const loader = async () => {
|
||||||
const { data } = await api.data.readMany("todos");
|
const api = await getApi();
|
||||||
return { data, user: api.getUser() };
|
|
||||||
|
const limit = 5;
|
||||||
|
const {
|
||||||
|
data: todos,
|
||||||
|
body: { meta }
|
||||||
|
} = await api.data.readMany("todos", {
|
||||||
|
limit,
|
||||||
|
sort: "-id"
|
||||||
|
});
|
||||||
|
|
||||||
|
return { todos: todos.reverse(), total: meta.total, limit };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const action = async (args: ActionFunctionArgs) => {
|
||||||
|
const api = await getApi();
|
||||||
|
const formData = await args.request.formData();
|
||||||
|
const action = formData.get("action") as string;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "update": {
|
||||||
|
const id = Number(formData.get("id"));
|
||||||
|
const done = formData.get("done") === "on";
|
||||||
|
|
||||||
|
if (id > 0) {
|
||||||
|
await api.data.updateOne("todos", id, { done });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "add": {
|
||||||
|
const title = formData.get("title") as string;
|
||||||
|
|
||||||
|
if (title.length > 0) {
|
||||||
|
await api.data.createOne("todos", { title });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "delete": {
|
||||||
|
const id = Number(formData.get("id"));
|
||||||
|
|
||||||
|
if (id > 0) {
|
||||||
|
await api.data.deleteOne("todos", id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { data, user } = useLoaderData<typeof loader>();
|
const ctx = useOutletContext<any>();
|
||||||
|
const { todos, total, limit } = useLoaderData<typeof loader>();
|
||||||
|
const fetcher = useFetcher();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex h-screen items-center justify-center">
|
||||||
<h1>Data</h1>
|
<div className="flex flex-col items-center gap-16">
|
||||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
<header className="flex flex-col items-center gap-9">
|
||||||
<h1>User</h1>
|
<h1 className="leading text-2xl font-bold text-gray-800 dark:text-gray-100">
|
||||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
bknd w/ <span className="sr-only">Remix</span>
|
||||||
|
</h1>
|
||||||
|
<div className="h-[144px] w-[434px]">
|
||||||
|
<img src="/logo-light.png" alt="Remix" className="block w-full dark:hidden" />
|
||||||
|
<img src="/logo-dark.png" alt="Remix" className="hidden w-full dark:block" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<nav className="flex flex-col items-center justify-center gap-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700">
|
||||||
|
<p className="leading-6 text-gray-700 dark:text-gray-200 font-bold">
|
||||||
|
What's next? ({total})
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col w-full gap-2">
|
||||||
|
{total > limit && (
|
||||||
|
<div className="bg-white/10 flex justify-center p-1 text-xs rounded text-gray-500">
|
||||||
|
{total - limit} more todo(s) hidden
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{todos.map((todo) => (
|
||||||
|
<div className="flex flex-row" key={String(todo.id)}>
|
||||||
|
<fetcher.Form
|
||||||
|
className="flex flex-row flex-grow items-center gap-3 ml-1"
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="done"
|
||||||
|
defaultChecked={todo.done}
|
||||||
|
onChange={(e) => fetcher.submit(e.currentTarget.form!)}
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="action" value="update" />
|
||||||
|
<input type="hidden" name="id" value={String(todo.id)} />
|
||||||
|
<div className="dark:text-gray-300 text-gray-800">{todo.title}</div>
|
||||||
|
</fetcher.Form>
|
||||||
|
<fetcher.Form className="flex items-center" method="post">
|
||||||
|
<input type="hidden" name="action" value="delete" />
|
||||||
|
<input type="hidden" name="id" value={String(todo.id)} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs "
|
||||||
|
>
|
||||||
|
❌
|
||||||
|
</button>
|
||||||
|
</fetcher.Form>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<fetcher.Form
|
||||||
|
className="flex flex-row gap-3 mt-2"
|
||||||
|
method="post"
|
||||||
|
key={todos.map((t) => t.id).join()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
placeholder="New todo"
|
||||||
|
className="py-2 px-4 rounded-xl bg-black/5 dark:bg-white/10"
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="action" value="add" />
|
||||||
|
<button type="submit" className="cursor-pointer">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</fetcher.Form>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<a href="/admin">Go to Admin ➝</a>
|
||||||
|
<div className="opacity-50 text-xs">
|
||||||
|
{ctx.user ? (
|
||||||
|
<p>
|
||||||
|
Authenticated as <b>{ctx.user.email}</b>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<a href="/admin/auth/login">Login</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import "bknd/dist/styles.css";
|
|||||||
export default adminPage({
|
export default adminPage({
|
||||||
config: {
|
config: {
|
||||||
basepath: "/admin",
|
basepath: "/admin",
|
||||||
logo_return_path: "/../"
|
logo_return_path: "/../",
|
||||||
|
color_scheme: "system"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,76 +1,9 @@
|
|||||||
import { App } from "bknd";
|
import { getApp } from "~/bknd";
|
||||||
import { registerLocalMediaAdapter } from "bknd/adapter/node";
|
|
||||||
import { serve } from "bknd/adapter/remix";
|
|
||||||
import { boolean, em, entity, text } from "bknd/data";
|
|
||||||
import { secureRandomString } from "bknd/utils";
|
|
||||||
|
|
||||||
// since we're running in node, we can register the local media adapter
|
const handler = async (args: { request: Request }) => {
|
||||||
registerLocalMediaAdapter();
|
const app = await getApp(args);
|
||||||
|
return app.fetch(args.request);
|
||||||
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 {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = serve({
|
|
||||||
// we can use any libsql config, and if omitted, uses in-memory
|
|
||||||
connection: {
|
|
||||||
url: "file:test.db"
|
|
||||||
},
|
|
||||||
// an initial config is only applied if the database is empty
|
|
||||||
initialConfig: {
|
|
||||||
data: schema.toJSON(),
|
|
||||||
// we're enabling auth ...
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
issuer: "bknd-remix-example",
|
|
||||||
secret: secureRandomString(64)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// ... and media
|
|
||||||
media: {
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./public"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const loader = handler;
|
export const loader = handler;
|
||||||
export const action = handler;
|
export const action = handler;
|
||||||
|
|||||||
10
examples/remix/app/tailwind.css
Normal file
10
examples/remix/app/tailwind.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
@apply bg-white dark:bg-gray-950;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "remix",
|
"name": "remix",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "remix vite:build",
|
"build": "remix vite:build",
|
||||||
"dev": "remix vite:dev",
|
"dev": "remix vite:dev",
|
||||||
"start": "remix-serve ./build/server/index.js",
|
"start": "remix-serve ./build/server/index.js",
|
||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/node": "^2.15.2",
|
"@remix-run/node": "^2.15.2",
|
||||||
"@remix-run/react": "^2.15.2",
|
"@remix-run/react": "^2.15.2",
|
||||||
"@remix-run/serve": "^2.15.2",
|
"@remix-run/serve": "^2.15.2",
|
||||||
"bknd": "file:../../app",
|
"bknd": "file:../../app",
|
||||||
"isbot": "^5.1.18",
|
"isbot": "^5.1.18",
|
||||||
"react": "file:../../node_modules/react",
|
"react": "file:../../node_modules/react",
|
||||||
"react-dom": "file:../../node_modules/react-dom",
|
"react-dom": "file:../../node_modules/react-dom",
|
||||||
"remix-utils": "^7.0.0"
|
"remix-utils": "^7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@remix-run/dev": "^2.15.2",
|
"@tailwindcss/vite": "^4.0.7",
|
||||||
"@types/react": "^18.2.20",
|
"tailwindcss": "^4.0.7",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@remix-run/dev": "^2.15.2",
|
||||||
"typescript": "^5.1.6",
|
"@types/react": "^18.2.20",
|
||||||
"vite": "^5.1.0",
|
"@types/react-dom": "^18.2.7",
|
||||||
"vite-tsconfig-paths": "^4.2.1"
|
"typescript": "^5.1.6",
|
||||||
},
|
"vite": "^5.1.0",
|
||||||
"engines": {
|
"vite-tsconfig-paths": "^4.2.1"
|
||||||
"node": ">=20.0.0"
|
},
|
||||||
}
|
"engines": {
|
||||||
}
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
examples/remix/public/logo-dark.png
Normal file
BIN
examples/remix/public/logo-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
BIN
examples/remix/public/logo-light.png
Normal file
BIN
examples/remix/public/logo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
@@ -1,4 +1,5 @@
|
|||||||
import { vitePlugin as remix } from "@remix-run/dev";
|
import { vitePlugin as remix } from "@remix-run/dev";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ declare module "@remix-run/node" {
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
remix({
|
remix({
|
||||||
future: {
|
future: {
|
||||||
v3_fetcherPersist: true,
|
v3_fetcherPersist: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user