From 282c9f6054b38bfb5f5dfc325e047e75fbf1dfee Mon Sep 17 00:00:00 2001
From: Shishant Biswas
Date: Sun, 27 Sep 2026 17:57:45 +0530
Subject: [PATCH] fix: on delete
---
.vscode/settings.json | 13 +-
app/__test__/adapter/adapter.test.ts | 107 ++-
app/__test__/api/Api.spec.ts | 12 +-
app/__test__/auth/Authenticator.spec.ts | 22 +-
app/__test__/data/polymorphic.test.ts | 1 +
app/build.ts | 614 +++++++++---------
app/e2e/adapters.ts | 5 +-
app/e2e/inc/adapters.ts | 78 +--
app/src/adapter/nuxt/nuxt.adapter.ts | 5 +-
.../tanstack-start/tanstack-start.adapter.ts | 21 +-
app/src/auth/middlewares/auth.middleware.ts | 4 +-
app/src/core/drivers/email/plunk.spec.ts | 86 ++-
app/src/core/drivers/email/plunk.ts | 102 +--
app/src/core/utils/uuid.ts | 6 +-
.../postgres/PgPostgresConnection.ts | 6 +-
.../postgres/PostgresJsConnection.ts | 4 +-
app/src/data/entities/Entity.ts | 5 +-
app/src/data/entities/EntityManager.ts | 6 +-
app/src/data/relations/EntityRelation.ts | 3 +-
app/src/data/relations/ManyToOneRelation.ts | 3 +-
app/src/data/relations/RelationField.ts | 18 +-
app/src/data/relations/index.ts | 4 +
app/src/data/relations/relation-types.ts | 10 +
app/src/ui/Admin.tsx | 6 +-
app/src/ui/client/index.ts | 2 +-
app/src/ui/client/utils/AppReduced.ts | 1 -
app/src/ui/components/display/Logo.tsx | 6 +-
.../form/json-schema-form/ArrayField.tsx | 6 +-
app/src/ui/components/steps/Steps.tsx | 7 +-
app/src/ui/elements/media/helper.ts | 7 +-
app/src/ui/lib/routes.ts | 7 +-
.../ui/modules/data/components/EntityForm.tsx | 10 +-
.../schema/create-modal/step.relation.tsx | 180 ++---
app/src/ui/routes/data/_data.root.tsx | 13 +-
.../routes/settings/routes/data.settings.tsx | 5 +-
.../test/tests/dropzone-element-test.tsx | 6 +-
app/src/ui/styles.css | 4 +-
app/src/ui/types.ts | 2 +-
38 files changed, 742 insertions(+), 655 deletions(-)
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 5c3e1c66..f769d3b0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,19 +1,22 @@
{
- "typescript.tsdk": "node_modules/typescript/lib",
+ "js/ts.tsdk.path": "node_modules/typescript/lib",
"biome.enabled": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
//"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
},
- "typescript.preferences.importModuleSpecifier": "non-relative",
- "typescript.preferences.autoImportFileExcludePatterns": [
+ "js/ts.preferences.importModuleSpecifier": "non-relative",
+ "js/ts.preferences.autoImportFileExcludePatterns": [
"**/dist/**",
"**/node_modules/**/dist/**",
"**/node_modules/**/!(src|lib|esm)/**" // optional, stricter
],
- "typescript.preferences.includePackageJsonAutoImports": "on",
- "typescript.tsserver.watchOptions": {
+ "js/ts.preferences.includePackageJsonAutoImports": "on",
+ "js/ts.tsserver.watchOptions": {
"excludeDirectories": ["**/dist", "**/node_modules/**/dist"]
+ },
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "biomejs.biome"
}
}
diff --git a/app/__test__/adapter/adapter.test.ts b/app/__test__/adapter/adapter.test.ts
index 8e8eb993..28c3263e 100644
--- a/app/__test__/adapter/adapter.test.ts
+++ b/app/__test__/adapter/adapter.test.ts
@@ -4,73 +4,72 @@ import { disableConsoleLog, enableConsoleLog, omitKeys } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
-const stripConnection = >(cfg: T) =>
- omitKeys(cfg, ["connection"]);
+const stripConnection = >(cfg: T) => omitKeys(cfg, ["connection"]);
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("adapter", () => {
- describe("makeConfig", () => {
- it("returns empty config for empty inputs", async () => {
- const cases: Array> = [
- [{}],
- [{}, { env: { TEST: "test" } }],
- ];
+ describe("makeConfig", () => {
+ it("returns empty config for empty inputs", async () => {
+ const cases: Array> = [
+ [{}],
+ [{}, { env: { TEST: "test" } }],
+ ];
- for (const args of cases) {
- const cfg = await adapter.makeConfig(...(args as any));
- expect(stripConnection(cfg)).toEqual({});
- }
- });
-
- it("merges app output into config", async () => {
- const cfg = await adapter.makeConfig(
- { app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
- { env: { TEST: "test" } },
- );
-
- expect(stripConnection(cfg)).toEqual({
- config: { server: { cors: { origin: "test" } } },
+ for (const args of cases) {
+ const cfg = await adapter.makeConfig(...(args as any));
+ expect(stripConnection(cfg)).toEqual({});
+ }
});
- });
- it("allows all properties in app() result", async () => {
- const called = mock(() => null);
+ it("merges app output into config", async () => {
+ const cfg = await adapter.makeConfig(
+ { app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
+ { env: { TEST: "test" } },
+ );
- const cfg = await adapter.makeConfig(
- {
- app: (env) => ({
- connection: { url: "test" },
+ expect(stripConnection(cfg)).toEqual({
config: { server: { cors: { origin: "test" } } },
- options: { mode: "db" as const },
- onBuilt: () => {
- called();
- expect(env).toEqual({ foo: "bar" });
+ });
+ });
+
+ it("allows all properties in app() result", async () => {
+ const called = mock(() => null);
+
+ const cfg = await adapter.makeConfig(
+ {
+ app: (env) => ({
+ connection: { url: "test" },
+ config: { server: { cors: { origin: "test" } } },
+ options: { mode: "db" as const },
+ onBuilt: () => {
+ called();
+ expect(env).toEqual({ foo: "bar" });
+ },
+ }),
},
- }),
- },
- { foo: "bar" },
- );
+ { foo: "bar" },
+ );
- expect(cfg.connection).toEqual({ url: "test" });
- expect(cfg.config).toEqual({ server: { cors: { origin: "test" } } });
- expect(cfg.options).toEqual({ mode: "db" });
+ expect(cfg.connection).toEqual({ url: "test" });
+ expect(cfg.config).toEqual({ server: { cors: { origin: "test" } } });
+ expect(cfg.options).toEqual({ mode: "db" });
- await cfg.onBuilt?.({} as any);
- expect(called).toHaveBeenCalledTimes(1);
- });
- });
+ await cfg.onBuilt?.({} as any);
+ expect(called).toHaveBeenCalledTimes(1);
+ });
+ });
- describe("adapter test suites", () => {
- adapterTestSuite(bunTestRunner, {
- makeApp: adapter.createFrameworkApp,
- label: "framework app",
- });
+ describe("adapter test suites", () => {
+ adapterTestSuite(bunTestRunner, {
+ makeApp: adapter.createFrameworkApp,
+ label: "framework app",
+ });
- adapterTestSuite(bunTestRunner, {
- makeApp: adapter.createRuntimeApp,
- label: "runtime app",
- });
- });
+ adapterTestSuite(bunTestRunner, {
+ makeApp: adapter.createRuntimeApp,
+ label: "runtime app",
+ });
+ });
});
diff --git a/app/__test__/api/Api.spec.ts b/app/__test__/api/Api.spec.ts
index 7da7fbc0..392ac91f 100644
--- a/app/__test__/api/Api.spec.ts
+++ b/app/__test__/api/Api.spec.ts
@@ -73,7 +73,13 @@ describe("Api", async () => {
it("should extract tokens case insensitive", async () => {
const token = await sign({ sub: "test" }, "1234");
- expect(new Api({ headers: new Headers({ Authorization: `Bearer ${token}` }) }).getAuthState().token).toBe(token);
- expect(new Api({ headers: new Headers({ Authorization: `bearer ${token}` }) }).getAuthState().token).toBe(token);
- })
+ expect(
+ new Api({ headers: new Headers({ Authorization: `Bearer ${token}` }) }).getAuthState()
+ .token,
+ ).toBe(token);
+ expect(
+ new Api({ headers: new Headers({ Authorization: `bearer ${token}` }) }).getAuthState()
+ .token,
+ ).toBe(token);
+ });
});
diff --git a/app/__test__/auth/Authenticator.spec.ts b/app/__test__/auth/Authenticator.spec.ts
index 171614dc..c6f4ad59 100644
--- a/app/__test__/auth/Authenticator.spec.ts
+++ b/app/__test__/auth/Authenticator.spec.ts
@@ -51,14 +51,18 @@ describe("Authenticator", async () => {
});
const token = await auth.jwt({ sub: "test" });
- const res = await auth.resolveAuthFromRequest(new Headers({
- Authorization: `Bearer ${token}`,
- }));
- expect((res as any).sub).toBe("test")
+ const res = await auth.resolveAuthFromRequest(
+ new Headers({
+ Authorization: `Bearer ${token}`,
+ }),
+ );
+ expect((res as any).sub).toBe("test");
- const res2 = await auth.resolveAuthFromRequest(new Headers({
- Authorization: `bearer ${token}`,
- }));
- expect((res2 as any).sub).toBe("test")
- })
+ const res2 = await auth.resolveAuthFromRequest(
+ new Headers({
+ Authorization: `bearer ${token}`,
+ }),
+ );
+ expect((res2 as any).sub).toBe("test");
+ });
});
diff --git a/app/__test__/data/polymorphic.test.ts b/app/__test__/data/polymorphic.test.ts
index 5777801b..7a3c063b 100644
--- a/app/__test__/data/polymorphic.test.ts
+++ b/app/__test__/data/polymorphic.test.ts
@@ -26,6 +26,7 @@ describe("Polymorphic", async () => {
target: "media",
config: {
mappedBy: "image",
+ on_delete: "set null",
},
});
// media should not see categories
diff --git a/app/build.ts b/app/build.ts
index 23986b9d..04cb3368 100644
--- a/app/build.ts
+++ b/app/build.ts
@@ -14,15 +14,15 @@ const clean = args.includes("--clean");
// silence tsup
const oldConsole = {
- log: console.log,
- warn: console.warn,
+ log: console.log,
+ warn: console.warn,
};
console.log = () => {};
console.warn = () => {};
const define = {
- __isDev: "0",
- __version: JSON.stringify(pkg.version),
+ __isDev: "0",
+ __version: JSON.stringify(pkg.version),
};
if (clean) {
@@ -43,149 +43,146 @@ if (clean) {
let types_running = false;
function buildTypes() {
- if (types_running || !types) return;
- types_running = true;
+ if (types_running || !types) return;
+ types_running = true;
- Bun.spawn(["bun", "build:types"], {
- stdout: "inherit",
- onExit: () => {
- oldConsole.log(c.cyan("[Types]"), c.green("built"));
- Bun.spawn(["bun", "tsc-alias"], {
- stdout: "inherit",
- onExit: () => {
- oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
- types_running = false;
- },
- });
- },
- });
+ Bun.spawn(["bun", "build:types"], {
+ stdout: "inherit",
+ onExit: () => {
+ oldConsole.log(c.cyan("[Types]"), c.green("built"));
+ Bun.spawn(["bun", "tsc-alias"], {
+ stdout: "inherit",
+ onExit: () => {
+ oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
+ types_running = false;
+ },
+ });
+ },
+ });
}
if (types && !watch) {
- buildTypes();
+ buildTypes();
}
let watcher_timeout: any;
function delayTypes() {
- if (!watch || !types) return;
- if (watcher_timeout) {
- clearTimeout(watcher_timeout);
- }
- watcher_timeout = setTimeout(buildTypes, 1000);
+ if (!watch || !types) return;
+ if (watcher_timeout) {
+ clearTimeout(watcher_timeout);
+ }
+ watcher_timeout = setTimeout(buildTypes, 1000);
}
const dependencies = Object.keys(pkg.dependencies);
// collection of always-external packages
const external = [
- ...dependencies,
- "bun:test",
- "node:test",
- "node:assert/strict",
- "@libsql/client",
- "bknd",
- /^bknd\/.*/,
- "jsonv-ts",
- /^jsonv-ts\/.*/,
+ ...dependencies,
+ "bun:test",
+ "node:test",
+ "node:assert/strict",
+ "@libsql/client",
+ "bknd",
+ /^bknd\/.*/,
+ "jsonv-ts",
+ /^jsonv-ts\/.*/,
] as const;
/**
* Building backend and general API
*/
async function buildApi() {
- await tsup.build({
- minify,
- sourcemap,
- // don't use tsup's broken watch, we'll handle it ourselves
- watch: false,
- define,
- entry: [
- "src/index.ts",
- "src/core/utils/index.ts",
- "src/plugins/index.ts",
- "src/modes/index.ts",
- ],
- outDir: "dist",
- external: [...external],
- metafile: true,
- target: "esnext",
- platform: "browser",
- removeNodeProtocol: false,
- format: ["esm"],
- splitting: false,
- loader: {
- ".svg": "dataurl",
- },
- onSuccess: async () => {
- delayTypes();
- oldConsole.log(c.cyan("[API]"), c.green("built"));
- },
- });
+ await tsup.build({
+ minify,
+ sourcemap,
+ // don't use tsup's broken watch, we'll handle it ourselves
+ watch: false,
+ define,
+ entry: [
+ "src/index.ts",
+ "src/core/utils/index.ts",
+ "src/plugins/index.ts",
+ "src/modes/index.ts",
+ ],
+ outDir: "dist",
+ external: [...external],
+ metafile: true,
+ target: "esnext",
+ platform: "browser",
+ removeNodeProtocol: false,
+ format: ["esm"],
+ splitting: false,
+ loader: {
+ ".svg": "dataurl",
+ },
+ onSuccess: async () => {
+ delayTypes();
+ oldConsole.log(c.cyan("[API]"), c.green("built"));
+ },
+ });
}
async function rewriteClient(path: string) {
- const bundle = await Bun.file(path).text();
- await Bun.write(
- path,
- '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"),
- );
+ const bundle = await Bun.file(path).text();
+ await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
}
/**
* Building UI for direct imports
*/
async function buildUi() {
- const base = {
- minify,
- sourcemap,
- watch: false,
- define,
- external: [
- ...external,
- "react",
- "react-dom",
- "react/jsx-runtime",
- "react/jsx-dev-runtime",
- "use-sync-external-store",
- /codemirror/,
- "@xyflow/react",
- "@mantine/core",
- ],
- metafile: true,
- platform: "browser",
- format: ["esm"],
- splitting: false,
- bundle: true,
- treeshake: true,
- loader: {
- ".svg": "dataurl",
- },
- esbuildOptions: (options) => {
- options.logLevel = "silent";
- },
- } satisfies tsup.Options;
+ const base = {
+ minify,
+ sourcemap,
+ watch: false,
+ define,
+ external: [
+ ...external,
+ "react",
+ "react-dom",
+ "react/jsx-runtime",
+ "react/jsx-dev-runtime",
+ "use-sync-external-store",
+ /codemirror/,
+ "@xyflow/react",
+ "@mantine/core",
+ ],
+ metafile: true,
+ platform: "browser",
+ format: ["esm"],
+ splitting: false,
+ bundle: true,
+ treeshake: true,
+ loader: {
+ ".svg": "dataurl",
+ },
+ esbuildOptions: (options) => {
+ options.logLevel = "silent";
+ },
+ } satisfies tsup.Options;
- await tsup.build({
- ...base,
- entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
- outDir: "dist/ui",
- onSuccess: async () => {
- await rewriteClient("./dist/ui/index.js");
- delayTypes();
- oldConsole.log(c.cyan("[UI]"), c.green("built"));
- },
- });
+ await tsup.build({
+ ...base,
+ entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
+ outDir: "dist/ui",
+ onSuccess: async () => {
+ await rewriteClient("./dist/ui/index.js");
+ delayTypes();
+ oldConsole.log(c.cyan("[UI]"), c.green("built"));
+ },
+ });
- await tsup.build({
- ...base,
- entry: ["src/ui/client/index.ts"],
- outDir: "dist/ui/client",
- onSuccess: async () => {
- await rewriteClient("./dist/ui/client/index.js");
- delayTypes();
- oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
- },
- });
+ await tsup.build({
+ ...base,
+ entry: ["src/ui/client/index.ts"],
+ outDir: "dist/ui/client",
+ onSuccess: async () => {
+ await rewriteClient("./dist/ui/client/index.js");
+ delayTypes();
+ oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
+ },
+ });
}
/**
@@ -194,195 +191,186 @@ async function buildUi() {
* - ui/client is external, and after built replaced with "bknd/client"
*/
async function buildUiElements() {
- await tsup.build({
- minify,
- sourcemap,
- watch: false,
- define,
- entry: ["src/ui/elements/index.ts"],
- outDir: "dist/ui/elements",
- external: [
- "ui/client",
- "bknd",
- /^bknd\/.*/,
- "wouter",
- "react",
- "react-dom",
- "react/jsx-runtime",
- "react/jsx-dev-runtime",
- "use-sync-external-store",
- ],
- metafile: true,
- platform: "browser",
- format: ["esm"],
- splitting: false,
- bundle: true,
- treeshake: true,
- loader: {
- ".svg": "dataurl",
- },
- esbuildOptions: (options) => {
- options.alias = {
- // not important for elements, mock to reduce bundle
- "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
- };
- },
- onSuccess: async () => {
- await rewriteClient("./dist/ui/elements/index.js");
- delayTypes();
- oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
- },
- });
+ await tsup.build({
+ minify,
+ sourcemap,
+ watch: false,
+ define,
+ entry: ["src/ui/elements/index.ts"],
+ outDir: "dist/ui/elements",
+ external: [
+ "ui/client",
+ "bknd",
+ /^bknd\/.*/,
+ "wouter",
+ "react",
+ "react-dom",
+ "react/jsx-runtime",
+ "react/jsx-dev-runtime",
+ "use-sync-external-store",
+ ],
+ metafile: true,
+ platform: "browser",
+ format: ["esm"],
+ splitting: false,
+ bundle: true,
+ treeshake: true,
+ loader: {
+ ".svg": "dataurl",
+ },
+ esbuildOptions: (options) => {
+ options.alias = {
+ // not important for elements, mock to reduce bundle
+ "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
+ };
+ },
+ onSuccess: async () => {
+ await rewriteClient("./dist/ui/elements/index.js");
+ delayTypes();
+ oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
+ },
+ });
}
/**
* Building adapters
*/
-function baseConfig(
- adapter: string,
- overrides: Partial = {},
-): tsup.Options {
- return {
- minify,
- sourcemap,
- watch: false,
- entry: [`src/adapter/${adapter}/index.ts`],
- format: ["esm"],
- platform: "neutral",
- outDir: `dist/adapter/${adapter}`,
- metafile: true,
- splitting: false,
- removeNodeProtocol: false,
- onSuccess: async () => {
- delayTypes();
- oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
- },
- ...overrides,
- define: {
- ...define,
- ...overrides.define,
- },
- external: [
- /^cloudflare*/,
- /^@?hono.*?/,
- /^(bknd|react|next|node).*?/,
- /.*\.(html)$/,
- ...external,
- ...(Array.isArray(overrides.external) ? overrides.external : []),
- ],
- };
+function baseConfig(adapter: string, overrides: Partial = {}): tsup.Options {
+ return {
+ minify,
+ sourcemap,
+ watch: false,
+ entry: [`src/adapter/${adapter}/index.ts`],
+ format: ["esm"],
+ platform: "neutral",
+ outDir: `dist/adapter/${adapter}`,
+ metafile: true,
+ splitting: false,
+ removeNodeProtocol: false,
+ onSuccess: async () => {
+ delayTypes();
+ oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
+ },
+ ...overrides,
+ define: {
+ ...define,
+ ...overrides.define,
+ },
+ external: [
+ /^cloudflare*/,
+ /^@?hono.*?/,
+ /^(bknd|react|next|node).*?/,
+ /.*\.(html)$/,
+ ...external,
+ ...(Array.isArray(overrides.external) ? overrides.external : []),
+ ],
+ };
}
async function buildAdapters() {
- await Promise.all([
- // base adapter handles
- tsup.build({
- ...baseConfig(""),
- target: "esnext",
- platform: "neutral",
- entry: ["src/adapter/index.ts"],
- outDir: "dist/adapter",
- // only way to keep @vite-ignore comments
- minify: false,
- }),
-
- // specific adatpers
- tsup.build(baseConfig("react-router")),
- tsup.build(
- baseConfig("browser", {
- external: [/^sqlocal\/?.*?/, "wouter"],
+ await Promise.all([
+ // base adapter handles
+ tsup.build({
+ ...baseConfig(""),
+ target: "esnext",
+ platform: "neutral",
+ entry: ["src/adapter/index.ts"],
+ outDir: "dist/adapter",
+ // only way to keep @vite-ignore comments
+ minify: false,
}),
- ),
- tsup.build(
- baseConfig("bun", {
- external: [/^bun\:.*/],
+
+ // specific adatpers
+ tsup.build(baseConfig("react-router")),
+ tsup.build(
+ baseConfig("browser", {
+ external: [/^sqlocal\/?.*?/, "wouter"],
+ }),
+ ),
+ tsup.build(
+ baseConfig("bun", {
+ external: [/^bun\:.*/],
+ }),
+ ),
+ tsup.build(baseConfig("astro")),
+ tsup.build(baseConfig("aws")),
+ tsup.build(
+ baseConfig("cloudflare", {
+ external: ["wrangler", "node:process"],
+ }),
+ ),
+ tsup.build(
+ baseConfig("cloudflare/proxy", {
+ target: "esnext",
+ entry: ["src/adapter/cloudflare/proxy.ts"],
+ outDir: "dist/adapter/cloudflare",
+ metafile: false,
+ external: [/bknd/, "wrangler", "node:process"],
+ }),
+ ),
+
+ tsup.build({
+ ...baseConfig("vite"),
+ platform: "node",
}),
- ),
- tsup.build(baseConfig("astro")),
- tsup.build(baseConfig("aws")),
- tsup.build(
- baseConfig("cloudflare", {
- external: ["wrangler", "node:process"],
+
+ tsup.build({
+ ...baseConfig("nextjs"),
+ platform: "node",
}),
- ),
- tsup.build(
- baseConfig("cloudflare/proxy", {
- target: "esnext",
- entry: ["src/adapter/cloudflare/proxy.ts"],
- outDir: "dist/adapter/cloudflare",
- metafile: false,
- external: [/bknd/, "wrangler", "node:process"],
+
+ tsup.build({
+ ...baseConfig("tanstack-start"),
+ platform: "node",
}),
- ),
- tsup.build({
- ...baseConfig("vite"),
- platform: "node",
- }),
+ tsup.build({
+ ...baseConfig("sveltekit"),
+ platform: "node",
+ }),
- tsup.build({
- ...baseConfig("nextjs"),
- platform: "node",
- }),
+ tsup.build({
+ ...baseConfig("nuxt"),
+ platform: "node",
+ }),
- tsup.build({
- ...baseConfig("tanstack-start"),
- platform: "node",
- }),
+ tsup.build({
+ ...baseConfig("node"),
+ platform: "node",
+ }),
- tsup.build({
- ...baseConfig("sveltekit"),
- platform: "node",
- }),
+ tsup.build({
+ ...baseConfig("solid-start"),
+ platform: "node",
+ }),
- tsup.build({
- ...baseConfig("nuxt"),
- platform: "node",
- }),
+ tsup.build({
+ ...baseConfig("sqlite/edge"),
+ entry: ["src/adapter/sqlite/edge.ts"],
+ outDir: "dist/adapter/sqlite",
+ metafile: false,
+ }),
- tsup.build({
- ...baseConfig("node"),
- platform: "node",
- }),
+ tsup.build({
+ ...baseConfig("sqlite/node"),
+ entry: ["src/adapter/sqlite/node.ts"],
+ outDir: "dist/adapter/sqlite",
+ platform: "node",
+ metafile: false,
+ }),
- tsup.build({
- ...baseConfig("solid-start"),
- platform: "node",
- }),
-
- tsup.build({
- ...baseConfig("sqlite/edge"),
- entry: ["src/adapter/sqlite/edge.ts"],
- outDir: "dist/adapter/sqlite",
- metafile: false,
- }),
-
- tsup.build({
- ...baseConfig("sqlite/node"),
- entry: ["src/adapter/sqlite/node.ts"],
- outDir: "dist/adapter/sqlite",
- platform: "node",
- metafile: false,
- }),
-
- tsup.build({
- ...baseConfig("sqlite/bun"),
- entry: ["src/adapter/sqlite/bun.ts"],
- outDir: "dist/adapter/sqlite",
- metafile: false,
- external: [/^bun\:.*/],
- }),
-
- ]);
+ tsup.build({
+ ...baseConfig("sqlite/bun"),
+ entry: ["src/adapter/sqlite/bun.ts"],
+ outDir: "dist/adapter/sqlite",
+ metafile: false,
+ external: [/^bun\:.*/],
+ }),
+ ]);
}
async function buildAll() {
- await Promise.all([
- buildApi(),
- buildUi(),
- buildUiElements(),
- buildAdapters(),
- ]);
+ await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
}
// initial build
@@ -390,47 +378,39 @@ await buildAll();
// custom watcher since tsup's watch is broken in 8.3.5+
if (watch) {
- oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/...");
+ oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/...");
- let debounceTimer: ReturnType | null = null;
- let isBuilding = false;
+ let debounceTimer: ReturnType | null = null;
+ let isBuilding = false;
- const rebuild = async () => {
- if (isBuilding) return;
- isBuilding = true;
- oldConsole.log(c.cyan("[Watch]"), "rebuilding...");
- try {
- await buildAll();
- oldConsole.log(c.cyan("[Watch]"), c.green("done"));
- } catch (e) {
- oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e);
- }
- isBuilding = false;
- };
+ const rebuild = async () => {
+ if (isBuilding) return;
+ isBuilding = true;
+ oldConsole.log(c.cyan("[Watch]"), "rebuilding...");
+ try {
+ await buildAll();
+ oldConsole.log(c.cyan("[Watch]"), c.green("done"));
+ } catch (e) {
+ oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e);
+ }
+ isBuilding = false;
+ };
- const debouncedRebuild = () => {
- if (debounceTimer) clearTimeout(debounceTimer);
- debounceTimer = setTimeout(rebuild, 100);
- };
+ const debouncedRebuild = () => {
+ if (debounceTimer) clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(rebuild, 100);
+ };
- // watch src directory recursively
- fsWatch(
- join(import.meta.dir, "src"),
- { recursive: true },
- (event, filename) => {
+ // watch src directory recursively
+ fsWatch(join(import.meta.dir, "src"), { recursive: true }, (event, filename) => {
if (!filename) return;
// ignore non-source files
- if (
- !filename.endsWith(".ts") &&
- !filename.endsWith(".tsx") &&
- !filename.endsWith(".css")
- )
- return;
+ if (!filename.endsWith(".ts") && !filename.endsWith(".tsx") && !filename.endsWith(".css"))
+ return;
oldConsole.log(c.cyan("[Watch]"), c.dim(`${event}: ${filename}`));
debouncedRebuild();
- },
- );
+ });
- // keep process alive
- await new Promise(() => {});
+ // keep process alive
+ await new Promise(() => {});
}
diff --git a/app/e2e/adapters.ts b/app/e2e/adapters.ts
index 5518e893..aaae9284 100644
--- a/app/e2e/adapters.ts
+++ b/app/e2e/adapters.ts
@@ -3,7 +3,10 @@ import path from "node:path";
import c from "picocolors";
const basePath = new URL(import.meta.resolve("../../")).pathname.slice(0, -1);
-type RunOptions = Omit, "stdout" | "stderr">;
+type RunOptions = Omit<
+ Bun.SpawnOptions.SpawnOptions<"ignore", "pipe", "pipe">,
+ "stdout" | "stderr"
+>;
async function run(
cmd: string[] | string,
diff --git a/app/e2e/inc/adapters.ts b/app/e2e/inc/adapters.ts
index c113b69f..14301a6a 100644
--- a/app/e2e/inc/adapters.ts
+++ b/app/e2e/inc/adapters.ts
@@ -1,53 +1,53 @@
const adapter = process.env.TEST_ADAPTER;
const default_config = {
- media_adapter: "local",
- base_path: "",
+ media_adapter: "local",
+ base_path: "",
} as const;
const configs = {
- cloudflare: {
- media_adapter: "r2",
- },
- "react-router": {
- base_path: "/admin",
- },
- nextjs: {
- base_path: "/admin",
- },
- nuxt: {
- base_path: "/admin",
- },
+ cloudflare: {
+ media_adapter: "r2",
+ },
+ "react-router": {
+ base_path: "/admin",
+ },
+ nextjs: {
+ base_path: "/admin",
+ },
+ nuxt: {
+ base_path: "/admin",
+ },
"solid-start": {
base_path: "/admin",
},
- astro: {
- base_path: "/admin",
- },
- node: {
- base_path: "",
- },
- bun: {
- base_path: "",
- },
- "tanstack-start": {
- base_path: "/admin",
- },
+ astro: {
+ base_path: "/admin",
+ },
+ node: {
+ base_path: "",
+ },
+ bun: {
+ base_path: "",
+ },
+ "tanstack-start": {
+ base_path: "/admin",
+ },
};
export function getAdapterConfig(): typeof default_config {
- if (adapter) {
- if (!configs[adapter]) {
- console.warn(
- `Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
- );
- } else {
- return {
- ...default_config,
- ...configs[adapter],
- };
- }
- }
+ if (adapter) {
+ if (!configs[adapter]) {
+ console.warn(
+ `Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
+ );
+ } else {
+ return {
+ ...default_config,
+ ...configs[adapter],
+ };
+ }
+ }
- return default_config;
+ return default_config;
}
diff --git a/app/src/adapter/nuxt/nuxt.adapter.ts b/app/src/adapter/nuxt/nuxt.adapter.ts
index 426c735b..fc6e03eb 100644
--- a/app/src/adapter/nuxt/nuxt.adapter.ts
+++ b/app/src/adapter/nuxt/nuxt.adapter.ts
@@ -20,10 +20,7 @@ export async function getApp(
* @param config - bknd configuration
* @param args - environment variables
*/
-export function serve(
- config: NuxtBkndConfig = {} as NuxtBkndConfig,
- args: Env,
-) {
+export function serve(config: NuxtBkndConfig = {} as NuxtBkndConfig, args: Env) {
return async (request: Request) => {
return (await getApp(config, args)).fetch(request);
};
diff --git a/app/src/adapter/tanstack-start/tanstack-start.adapter.ts b/app/src/adapter/tanstack-start/tanstack-start.adapter.ts
index 55151fe3..38ae0082 100644
--- a/app/src/adapter/tanstack-start/tanstack-start.adapter.ts
+++ b/app/src/adapter/tanstack-start/tanstack-start.adapter.ts
@@ -2,8 +2,7 @@ import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
export type TanstackStartEnv = NodeJS.ProcessEnv;
-export type TanstackStartConfig =
- FrameworkBkndConfig;
+export type TanstackStartConfig = FrameworkBkndConfig;
/**
* Get bknd app instance
@@ -11,10 +10,10 @@ export type TanstackStartConfig =
* @param args - environment variables
*/
export async function getApp(
- config: TanstackStartConfig = {},
- args: Env = process.env as Env,
+ config: TanstackStartConfig = {},
+ args: Env = process.env as Env,
) {
- return await createFrameworkApp(config, args);
+ return await createFrameworkApp(config, args);
}
/**
@@ -23,11 +22,11 @@ export async function getApp(
* @param args - environment variables
*/
export function serve(
- config: TanstackStartConfig = {},
- args: Env = process.env as Env,
+ config: TanstackStartConfig = {},
+ args: Env = process.env as Env,
) {
- return async (request: Request) => {
- const app = await getApp(config, args);
- return app.fetch(request);
- };
+ return async (request: Request) => {
+ const app = await getApp(config, args);
+ return app.fetch(request);
+ };
}
diff --git a/app/src/auth/middlewares/auth.middleware.ts b/app/src/auth/middlewares/auth.middleware.ts
index eeebe454..d7373a7c 100644
--- a/app/src/auth/middlewares/auth.middleware.ts
+++ b/app/src/auth/middlewares/auth.middleware.ts
@@ -26,9 +26,7 @@ export function shouldSkip(c: Context, skip?: (string | RegExp)[]) {
return result;
}
-export const auth = (options?: {
- skip?: (string | RegExp)[];
-}) =>
+export const auth = (options?: { skip?: (string | RegExp)[] }) =>
createMiddleware(async (c, next) => {
if (!c.get("auth")) {
c.set("auth", {
diff --git a/app/src/core/drivers/email/plunk.spec.ts b/app/src/core/drivers/email/plunk.spec.ts
index 82fb5449..6d842c7c 100644
--- a/app/src/core/drivers/email/plunk.spec.ts
+++ b/app/src/core/drivers/email/plunk.spec.ts
@@ -4,52 +4,48 @@ import { plunkEmail } from "./plunk";
const ALL_TESTS = !!process.env.ALL_TESTS;
describe.skipIf(ALL_TESTS)("plunk", () => {
- it("should throw on failed", async () => {
- const driver = plunkEmail({ apiKey: "invalid" });
- expect(driver.send("foo@bar.com", "Test", "Test")).rejects.toThrow();
- });
+ it("should throw on failed", async () => {
+ const driver = plunkEmail({ apiKey: "invalid" });
+ expect(driver.send("foo@bar.com", "Test", "Test")).rejects.toThrow();
+ });
- it("should send an email", async () => {
- const driver = plunkEmail({
- apiKey: process.env.PLUNK_API_KEY!,
- from: undefined, // Default to what Plunk sets
- });
- const response = await driver.send(
- "help@bknd.io",
- "Test Email from Plunk",
- "This is a test email",
- );
- expect(response).toBeDefined();
- expect(response.success).toBe(true);
- expect(response.emails).toBeDefined();
- expect(response.timestamp).toBeDefined();
- });
+ it("should send an email", async () => {
+ const driver = plunkEmail({
+ apiKey: process.env.PLUNK_API_KEY!,
+ from: undefined, // Default to what Plunk sets
+ });
+ const response = await driver.send(
+ "help@bknd.io",
+ "Test Email from Plunk",
+ "This is a test email",
+ );
+ expect(response).toBeDefined();
+ expect(response.success).toBe(true);
+ expect(response.emails).toBeDefined();
+ expect(response.timestamp).toBeDefined();
+ });
- it("should send HTML email", async () => {
- const driver = plunkEmail({
- apiKey: process.env.PLUNK_API_KEY!,
- from: undefined,
- });
- const htmlBody = "Test Email
This is a test email
";
- const response = await driver.send(
- "help@bknd.io",
- "HTML Test",
- htmlBody,
- );
- expect(response).toBeDefined();
- expect(response.success).toBe(true);
- });
+ it("should send HTML email", async () => {
+ const driver = plunkEmail({
+ apiKey: process.env.PLUNK_API_KEY!,
+ from: undefined,
+ });
+ const htmlBody = "Test Email
This is a test email
";
+ const response = await driver.send("help@bknd.io", "HTML Test", htmlBody);
+ expect(response).toBeDefined();
+ expect(response.success).toBe(true);
+ });
- it("should send with text and html", async () => {
- const driver = plunkEmail({
- apiKey: process.env.PLUNK_API_KEY!,
- from: undefined,
- });
- const response = await driver.send("test@example.com", "Test Email", {
- text: "help@bknd.io",
- html: "This is HTML
",
- });
- expect(response).toBeDefined();
- expect(response.success).toBe(true);
- });
+ it("should send with text and html", async () => {
+ const driver = plunkEmail({
+ apiKey: process.env.PLUNK_API_KEY!,
+ from: undefined,
+ });
+ const response = await driver.send("test@example.com", "Test Email", {
+ text: "help@bknd.io",
+ html: "This is HTML
",
+ });
+ expect(response).toBeDefined();
+ expect(response.success).toBe(true);
+ });
});
diff --git a/app/src/core/drivers/email/plunk.ts b/app/src/core/drivers/email/plunk.ts
index a3c77612..f0c0755a 100644
--- a/app/src/core/drivers/email/plunk.ts
+++ b/app/src/core/drivers/email/plunk.ts
@@ -1,70 +1,70 @@
import type { IEmailDriver } from "./index";
export type PlunkEmailOptions = {
- apiKey: string;
- host?: string;
- from?: string;
+ apiKey: string;
+ host?: string;
+ from?: string;
};
export type PlunkEmailSendOptions = {
- subscribed?: boolean;
- name?: string;
- from?: string;
- reply?: string;
- headers?: Record;
+ subscribed?: boolean;
+ name?: string;
+ from?: string;
+ reply?: string;
+ headers?: Record;
};
export type PlunkEmailResponse = {
- success: boolean;
- emails: Array<{
- contact: {
- id: string;
- email: string;
- };
- email: string;
- }>;
- timestamp: string;
+ success: boolean;
+ emails: Array<{
+ contact: {
+ id: string;
+ email: string;
+ };
+ email: string;
+ }>;
+ timestamp: string;
};
export const plunkEmail = (
- config: PlunkEmailOptions,
+ config: PlunkEmailOptions,
): IEmailDriver => {
- const host = config.host ?? "https://api.useplunk.com/v1/send";
- const from = config.from;
+ const host = config.host ?? "https://api.useplunk.com/v1/send";
+ const from = config.from;
- return {
- send: async (
- to: string,
- subject: string,
- body: string | { text: string; html: string },
- options?: PlunkEmailSendOptions,
- ) => {
- const payload: any = {
- from,
- to,
- subject,
- };
+ return {
+ send: async (
+ to: string,
+ subject: string,
+ body: string | { text: string; html: string },
+ options?: PlunkEmailSendOptions,
+ ) => {
+ const payload: any = {
+ from,
+ to,
+ subject,
+ };
- if (typeof body === "string") {
- payload.body = body;
- } else {
- payload.body = body.html;
- }
+ if (typeof body === "string") {
+ payload.body = body;
+ } else {
+ payload.body = body.html;
+ }
- const res = await fetch(host, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${config.apiKey}`,
- },
- body: JSON.stringify({ ...payload, ...options }),
- });
+ const res = await fetch(host, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${config.apiKey}`,
+ },
+ body: JSON.stringify({ ...payload, ...options }),
+ });
- if (!res.ok) {
- throw new Error(`Plunk API error: ${await res.text()}`);
- }
+ if (!res.ok) {
+ throw new Error(`Plunk API error: ${await res.text()}`);
+ }
- return (await res.json()) as PlunkEmailResponse;
- },
- };
+ return (await res.json()) as PlunkEmailResponse;
+ },
+ };
};
diff --git a/app/src/core/utils/uuid.ts b/app/src/core/utils/uuid.ts
index 99511aff..ebd4c72c 100644
--- a/app/src/core/utils/uuid.ts
+++ b/app/src/core/utils/uuid.ts
@@ -2,15 +2,15 @@ import { v4, v7, validate, version as uuidVersion } from "uuid";
// generates v4
export function uuid(): string {
- return v4();
+ return v4();
}
// generates v7
export function uuidv7(): string {
- return v7();
+ return v7();
}
// validate uuid
export function uuidValidate(uuid: string, version: 4 | 7): boolean {
- return validate(uuid) && uuidVersion(uuid) === version;
+ return validate(uuid) && uuidVersion(uuid) === version;
}
diff --git a/app/src/data/connection/postgres/PgPostgresConnection.ts b/app/src/data/connection/postgres/PgPostgresConnection.ts
index f9b54663..26b3edfc 100644
--- a/app/src/data/connection/postgres/PgPostgresConnection.ts
+++ b/app/src/data/connection/postgres/PgPostgresConnection.ts
@@ -1,4 +1,8 @@
-import { Kysely, PostgresDialect, type PostgresDialectConfig as KyselyPostgresDialectConfig } from "kysely";
+import {
+ Kysely,
+ PostgresDialect,
+ type PostgresDialectConfig as KyselyPostgresDialectConfig,
+} from "kysely";
import { PostgresIntrospector } from "./PostgresIntrospector";
import { PostgresConnection, plugins } from "./PostgresConnection";
import { customIntrospector } from "../Connection";
diff --git a/app/src/data/connection/postgres/PostgresJsConnection.ts b/app/src/data/connection/postgres/PostgresJsConnection.ts
index 710b033e..d67b085d 100644
--- a/app/src/data/connection/postgres/PostgresJsConnection.ts
+++ b/app/src/data/connection/postgres/PostgresJsConnection.ts
@@ -24,8 +24,6 @@ export class PostgresJsConnection extends PostgresConnection {
if (!virtual && f.isVirtual()) return false;
if (!primary && f instanceof PrimaryField) return false;
diff --git a/app/src/data/entities/EntityManager.ts b/app/src/data/entities/EntityManager.ts
index 033d51a5..804cbd8a 100644
--- a/app/src/data/entities/EntityManager.ts
+++ b/app/src/data/entities/EntityManager.ts
@@ -45,9 +45,9 @@ export class EntityManager {
emgr?: EventManager,
) {
// add entities & relations
- entities.forEach((entity) => this.addEntity(entity));
- relations.forEach((relation) => this.addRelation(relation));
- indices.forEach((index) => this.addIndex(index));
+ entities.forEach((entity) => void this.addEntity(entity));
+ relations.forEach((relation) => void this.addRelation(relation));
+ indices.forEach((index) => void this.addIndex(index));
if (!Connection.isConnection(connection)) {
throw new UnableToConnectException("");
diff --git a/app/src/data/relations/EntityRelation.ts b/app/src/data/relations/EntityRelation.ts
index b0c3cc44..733b804f 100644
--- a/app/src/data/relations/EntityRelation.ts
+++ b/app/src/data/relations/EntityRelation.ts
@@ -8,7 +8,7 @@ import {
RelationHelper,
} from "../relations";
import type { RepoQuery } from "../server/query";
-import type { RelationType } from "./relation-types";
+import { DEFAULT_RELATION_CASCADE, RelationCascades, type RelationType } from "./relation-types";
const directions = ["source", "target"] as const;
export type TDirection = (typeof directions)[number];
@@ -35,6 +35,7 @@ export abstract class EntityRelation<
mappedBy: s.string().optional(),
inversedBy: s.string().optional(),
required: s.boolean().optional(),
+ on_delete: s.string({ enum: RelationCascades, default: DEFAULT_RELATION_CASCADE }).optional(),
});
// don't make protected, App requires it to instantiatable
diff --git a/app/src/data/relations/ManyToOneRelation.ts b/app/src/data/relations/ManyToOneRelation.ts
index c5ab4049..cd17423e 100644
--- a/app/src/data/relations/ManyToOneRelation.ts
+++ b/app/src/data/relations/ManyToOneRelation.ts
@@ -7,7 +7,7 @@ import { EntityRelation, type KyselyQueryBuilder } from "./EntityRelation";
import { EntityRelationAnchor } from "./EntityRelationAnchor";
import { RelationField, type RelationFieldBaseConfig } from "./RelationField";
import type { MutationInstructionResponse } from "./RelationMutator";
-import { type RelationType, RelationTypes } from "./relation-types";
+import { type RelationType, RelationCascades, RelationTypes } from "./relation-types";
/**
* Source entity receives the mapping field
@@ -31,6 +31,7 @@ export class ManyToOneRelation extends EntityRelation;
-export type RelationFieldBaseConfig = { label?: string };
+export type RelationFieldBaseConfig = {
+ label?: string;
+ on_delete?: RelationCascade;
+};
export class RelationField extends Field {
override readonly type = "relation";
@@ -39,6 +41,8 @@ export class RelationField extends Field {
return new RelationField(name, {
...config,
required: relation.required,
+ // the relation owns the referential action, the field only materializes it
+ on_delete: config?.on_delete ?? relation.config.on_delete,
reference: target.reference,
target: target.entity.name,
target_field: target.entity.getPrimaryField().name,
@@ -50,6 +54,10 @@ export class RelationField extends Field {
return this.config.reference;
}
+ onDelete(): RelationCascade | undefined {
+ return this.config.on_delete;
+ }
+
target() {
return this.config.target;
}
@@ -63,7 +71,7 @@ export class RelationField extends Field {
...super.schema()!,
type: this.config.target_field_type ?? "integer",
references: `${this.config.target}.${this.config.target_field}`,
- onDelete: this.config.on_delete ?? "set null",
+ onDelete: this.config.on_delete ?? DEFAULT_RELATION_CASCADE,
});
}
diff --git a/app/src/data/relations/index.ts b/app/src/data/relations/index.ts
index 6f4c3a5c..834dc2e4 100644
--- a/app/src/data/relations/index.ts
+++ b/app/src/data/relations/index.ts
@@ -3,6 +3,7 @@ import { ManyToOneRelation, type ManyToOneRelationConfig } from "./ManyToOneRela
import { OneToOneRelation, type OneToOneRelationConfig } from "./OneToOneRelation";
import { PolymorphicRelation, type PolymorphicRelationConfig } from "./PolymorphicRelation";
import { type RelationType, RelationTypes } from "./relation-types";
+import { DEFAULT_RELATION_CASCADE, RelationCascades, type RelationCascade } from "./relation-types";
export * from "./EntityRelation";
export * from "./EntityRelationAnchor";
@@ -28,6 +29,9 @@ export {
type PolymorphicRelationConfig,
RelationTypes,
type RelationType,
+ RelationCascades,
+ type RelationCascade,
+ DEFAULT_RELATION_CASCADE,
// field
RelationField,
relationFieldConfigSchema,
diff --git a/app/src/data/relations/relation-types.ts b/app/src/data/relations/relation-types.ts
index ff17cfca..bd297c95 100644
--- a/app/src/data/relations/relation-types.ts
+++ b/app/src/data/relations/relation-types.ts
@@ -5,3 +5,13 @@ export const RelationTypes = {
Polymorphic: "poly",
} as const;
export type RelationType = (typeof RelationTypes)[keyof typeof RelationTypes];
+
+export const RelationCascades = [
+ "cascade",
+ "set null",
+ "set default",
+ "restrict",
+ "no action",
+] as const;
+export type RelationCascade = (typeof RelationCascades)[number];
+export const DEFAULT_RELATION_CASCADE: RelationCascade = "set null";
diff --git a/app/src/ui/Admin.tsx b/app/src/ui/Admin.tsx
index c62eedce..82293c55 100644
--- a/app/src/ui/Admin.tsx
+++ b/app/src/ui/Admin.tsx
@@ -17,9 +17,9 @@ export type BkndAdminConfig = {
*/
basepath?: string;
/**
- * Sub-path for the Admin UI within the base path
- * @default ``
- */
+ * Sub-path for the Admin UI within the base path
+ * @default ``
+ */
admin_basepath?: string;
/**
* Path to return to when clicking the logo
diff --git a/app/src/ui/client/index.ts b/app/src/ui/client/index.ts
index f6117523..a667e3dc 100644
--- a/app/src/ui/client/index.ts
+++ b/app/src/ui/client/index.ts
@@ -4,7 +4,7 @@ export {
type ClientProviderProps,
useApi,
useBaseUrl,
- useClientContext
+ useClientContext,
} from "./ClientProvider";
export * from "./api/use-api";
diff --git a/app/src/ui/client/utils/AppReduced.ts b/app/src/ui/client/utils/AppReduced.ts
index 3b493ce7..c64bbae8 100644
--- a/app/src/ui/client/utils/AppReduced.ts
+++ b/app/src/ui/client/utils/AppReduced.ts
@@ -22,7 +22,6 @@ export class AppReduced {
protected appJson: AppType,
protected _options: BkndAdminProps["config"] = {},
) {
-
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
return constructEntity(name, entity);
});
diff --git a/app/src/ui/components/display/Logo.tsx b/app/src/ui/components/display/Logo.tsx
index 54d90aef..ff74ef71 100644
--- a/app/src/ui/components/display/Logo.tsx
+++ b/app/src/ui/components/display/Logo.tsx
@@ -4,7 +4,11 @@ export function Logo({
scale = 0.2,
fill,
...props
-}: { scale?: number; fill?: string; theme?: string }) {
+}: {
+ scale?: number;
+ fill?: string;
+ theme?: string;
+}) {
const t = useTheme();
const theme = props.theme ?? t.theme;
const svgFill = fill ? fill : theme === "light" ? "black" : "white";
diff --git a/app/src/ui/components/form/json-schema-form/ArrayField.tsx b/app/src/ui/components/form/json-schema-form/ArrayField.tsx
index 9949691a..04227e0a 100644
--- a/app/src/ui/components/form/json-schema-form/ArrayField.tsx
+++ b/app/src/ui/components/form/json-schema-form/ArrayField.tsx
@@ -117,7 +117,11 @@ const ArrayAdd = ({
schema,
path: _path,
label = "Add",
-}: { schema: JsonSchema; path: string; label?: string }) => {
+}: {
+ schema: JsonSchema;
+ path: string;
+ label?: string;
+}) => {
const {
setValue,
value: { currentIndex },
diff --git a/app/src/ui/components/steps/Steps.tsx b/app/src/ui/components/steps/Steps.tsx
index 7422520b..3453cfb6 100644
--- a/app/src/ui/components/steps/Steps.tsx
+++ b/app/src/ui/components/steps/Steps.tsx
@@ -64,6 +64,11 @@ export function Step({
path = [],
id,
...rest
-}: { children: React.ReactNode; disabled?: boolean; id: string; path?: string[] }) {
+}: {
+ children: React.ReactNode;
+ disabled?: boolean;
+ id: string;
+ path?: string[];
+}) {
return {children}
;
}
diff --git a/app/src/ui/elements/media/helper.ts b/app/src/ui/elements/media/helper.ts
index ceefdebf..0cec4f80 100644
--- a/app/src/ui/elements/media/helper.ts
+++ b/app/src/ui/elements/media/helper.ts
@@ -35,7 +35,12 @@ export function checkMaxReached({
current = 0,
overwrite,
added,
-}: { maxItems?: number; current?: number; overwrite?: boolean; added: number }) {
+}: {
+ maxItems?: number;
+ current?: number;
+ overwrite?: boolean;
+ added: number;
+}) {
if (!maxItems) {
return {
reject: false,
diff --git a/app/src/ui/lib/routes.ts b/app/src/ui/lib/routes.ts
index a2b5bcc6..2d9f9d86 100644
--- a/app/src/ui/lib/routes.ts
+++ b/app/src/ui/lib/routes.ts
@@ -101,7 +101,12 @@ export function useNavigate() {
}
}
- const _url = options?.absolute ? `~/${app.options.basepath}/${app.options.admin_basepath}${url}`.replace(/\/+/g, "/") : url;
+ const _url = options?.absolute
+ ? `~/${app.options.basepath}/${app.options.admin_basepath}${url}`.replace(
+ /\/+/g,
+ "/",
+ )
+ : url;
const state = {
...options?.state,
referrer: location,
diff --git a/app/src/ui/modules/data/components/EntityForm.tsx b/app/src/ui/modules/data/components/EntityForm.tsx
index b1d741d2..194e23ec 100644
--- a/app/src/ui/modules/data/components/EntityForm.tsx
+++ b/app/src/ui/modules/data/components/EntityForm.tsx
@@ -283,7 +283,10 @@ function EntityJsonFormField({
fieldApi,
field,
...props
-}: { fieldApi: TFieldApi; field: JsonField }) {
+}: {
+ fieldApi: TFieldApi;
+ field: JsonField;
+}) {
const [error, setError] = useState(null);
const handleUpdate = useEvent((value: any) => {
setError(null);
@@ -312,7 +315,10 @@ function EntityEnumFormField({
fieldApi,
field,
...props
-}: { fieldApi: TFieldApi; field: EnumField }) {
+}: {
+ fieldApi: TFieldApi;
+ field: EnumField;
+}) {
const handleUpdate = useEvent((e: React.ChangeEvent) => {
fieldApi.handleChange(e.target.value);
});
diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx
index 6685d98c..6dd36947 100644
--- a/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx
+++ b/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx
@@ -1,6 +1,12 @@
import { Switch, TextInput } from "@mantine/core";
import { IconDatabase } from "@tabler/icons-react";
-import { ManyToOneRelation, type RelationType, RelationTypes } from "data/relations";
+import {
+ DEFAULT_RELATION_CASCADE,
+ ManyToOneRelation,
+ type RelationType,
+ RelationCascades,
+ RelationTypes,
+} from "data/relations";
import type { ReactNode } from "react";
import { type Control, type FieldValues, type UseFormRegister, useForm } from "react-hook-form";
import { TbRefresh } from "react-icons/tb";
@@ -46,7 +52,9 @@ const schema = s.strictObject({
type: s.string({ enum: Relations.map((r) => r.type) }),
source: stringIdentifier,
target: stringIdentifier,
- config: s.object({}),
+ config: s.object({
+ on_delete: s.string({ enum: RelationCascades }).optional(),
+ }),
});
type ComponentCtx = {
@@ -186,6 +194,25 @@ const Callout = ({ children }: { children: ReactNode }) => (
{children}
);
+function OnDelete({
+ control,
+ label = "On target delete",
+}: {
+ control: Control;
+ label?: string;
+}) {
+ return (
+ ({ value: cascade, label: cascade }))}
+ />
+ );
+}
+
function ManyToOne({ register, control, data: { source, target, config } }: ComponentCtx) {
return (
<>
@@ -209,6 +236,7 @@ function ManyToOne({ register, control, data: { source, target, config } }: Comp
control={control}
placeholder={String(ManyToOneRelation.DEFAULTS.with_limit)}
/>
+
@@ -222,30 +250,32 @@ function ManyToOne({ register, control, data: { source, target, config } }: Comp
{source && target && config && (
- <>
-
-
- {source}.{config.mappedBy || target}_id {"→"} {target}
-
+
+
+ {source}.{config.mappedBy || target}_id {"→"} {target}
+
+
+ Many
{source} will each have one reference to {target}.
+
+
+ A property
{config.mappedBy || target}_id will be added to{" "}
+ {source} (which references {target}).
+
+
+ When creating
{source}, a reference to {target} is{" "}
+ {config.required ? "required" : "optional"}.
+
+
+ If a
{target} entry is deleted, the reference is{" "}
+ {config.on_delete ?? DEFAULT_RELATION_CASCADE}.
+
+ {config.sourceCardinality ? (
- Many
{source} will each have one reference to {target}.
+ {target} should not have more than{" "}
+ {config.sourceCardinality} referencing entr
+ {config.sourceCardinality === 1 ? "y" : "ies"} to {source}.
-
- A property
{config.mappedBy || target}_id will be added to{" "}
- {source} (which references {target}).
-
-
- When creating
{source}, a reference to {target} is{" "}
- {config.required ? "required" : "optional"}.
-
- {config.sourceCardinality ? (
-
-
{target} should not have more than{" "}
- {config.sourceCardinality} referencing entr
- {config.sourceCardinality === 1 ? "y" : "ies"} to {source}.
-
- ) : null}
- >
+ ) : null}
)}
>
@@ -258,7 +288,7 @@ function OneToOne({
data: {
source,
target,
- config: { mappedBy, required },
+ config: { mappedBy, required, on_delete },
},
}: ComponentCtx) {
return (
@@ -271,6 +301,7 @@ function OneToOne({
placeholder={target}
/>
+
@@ -283,24 +314,26 @@ function OneToOne({
{source && target && (
- <>
-
-
- {source}.{mappedBy || target}_id {"↔"} {target}
-
-
- A single entry of
{source} will have a reference to{" "}
- {target}.
-
-
- A property
{mappedBy || target}_id will be added to{" "}
- {source} (which references {target}).
-
-
- When creating
{source}, a reference to {target} is{" "}
- {required ? "required" : "optional"}.
-
- >
+
+
+ {source}.{mappedBy || target}_id {"↔"} {target}
+
+
+ A single entry of
{source} will have a reference to {target}
+ .
+
+
+ A property
{mappedBy || target}_id will be added to {source}{" "}
+ (which references {target}).
+
+
+ When creating
{source}, a reference to {target} is{" "}
+ {required ? "required" : "optional"}.
+
+
+ If a
{target} entry is deleted, the reference is{" "}
+ {on_delete ?? DEFAULT_RELATION_CASCADE}.
+
)}
>
@@ -334,6 +367,7 @@ function ManyToMany({ register, control, data: { source, target, config } }: Com
{...register("config.connectionTableMappedName")}
placeholder={table}
/>
+
{/*
{source && target && (
- <>
-
-
- {source} {"→"} {table} {"←"} {target}
-
-
- Many
{source} will have many {target}.
-
-
- A connection table
{table} will be created to store the relations.
-
- >
+
+
+ {source} {"→"} {table} {"←"} {target}
+
+
+ Many
{source} will have many {target}.
+
+
+ A connection table
{table} will be created to store the relations.
+
+
+ If an entry of
{source} or {target} is deleted, the
+ connection is {config.on_delete ?? DEFAULT_RELATION_CASCADE}.
+
)}
>
@@ -391,26 +427,24 @@ function Polymorphic({ register, control, data: { type, source, target, config }
{source && target && (
- <>
-
-
- {source} {"←"} {target}
-
+
+
+ {source} {"←"} {target}
+
+
+
{source} will have many {target}.
+
+
+
{target} will get additional properties reference
and{" "}
+ entity_id
to make the (polymorphic) reference.
+
+ {config.targetCardinality ? (
-
{source} will have many {target}.
+ {source} should not have more than{" "}
+ {config.targetCardinality} reference
+ {config.targetCardinality === 1 ? "" : "s"} to {target}.
-
-
{target} will get additional properties reference
and{" "}
- entity_id
to make the (polymorphic) reference.
-
- {config.targetCardinality ? (
-
-
{source} should not have more than{" "}
- {config.targetCardinality} reference
- {config.targetCardinality === 1 ? "" : "s"} to {target}.
-
- ) : null}
- >
+ ) : null}
)}
>
diff --git a/app/src/ui/routes/data/_data.root.tsx b/app/src/ui/routes/data/_data.root.tsx
index ba27bf91..55ac6895 100644
--- a/app/src/ui/routes/data/_data.root.tsx
+++ b/app/src/ui/routes/data/_data.root.tsx
@@ -110,7 +110,12 @@ const EntityLinkList = ({
title,
context,
suggestCreate = false,
-}: { entities: Entity[]; title?: string; context: "data" | "schema"; suggestCreate?: boolean }) => {
+}: {
+ entities: Entity[];
+ title?: string;
+ context: "data" | "schema";
+ suggestCreate?: boolean;
+}) => {
const { $data } = useBkndData();
const { readonly } = useBknd();
const navigate = useRouteNavigate();
@@ -195,7 +200,11 @@ const EntityContextMenu = ({
entity,
children,
enabled = true,
-}: { entity: Entity; children: DropdownClickableChild; enabled?: boolean }) => {
+}: {
+ entity: Entity;
+ children: DropdownClickableChild;
+ enabled?: boolean;
+}) => {
if (!enabled) return children;
const [navigate] = useNavigate();
const { $data } = useBkndData();
diff --git a/app/src/ui/routes/settings/routes/data.settings.tsx b/app/src/ui/routes/settings/routes/data.settings.tsx
index 591da7af..7b56af65 100644
--- a/app/src/ui/routes/settings/routes/data.settings.tsx
+++ b/app/src/ui/routes/settings/routes/data.settings.tsx
@@ -66,7 +66,10 @@ const relationAnyOfValues = {
export const DataSettings = ({
schema,
config,
-}: { schema: ModuleSchemas["data"]; config: ModuleConfigs["data"] }) => {
+}: {
+ schema: ModuleSchemas["data"];
+ config: ModuleConfigs["data"];
+}) => {
const { app, readonly } = useBknd();
const prefix = app.getAbsolutePath("settings");
const entities = Object.keys(config.entities ?? {});
diff --git a/app/src/ui/routes/test/tests/dropzone-element-test.tsx b/app/src/ui/routes/test/tests/dropzone-element-test.tsx
index 3b9842da..867f9e06 100644
--- a/app/src/ui/routes/test/tests/dropzone-element-test.tsx
+++ b/app/src/ui/routes/test/tests/dropzone-element-test.tsx
@@ -69,11 +69,7 @@ function CustomUserAvatarDropzone() {
showPlaceholder,
actions: { openFileInput },
} = Media.useDropzone();
- const {
- isOver,
- isOverAccepted,
- files: [file] = [],
- } = Media.useDropzoneState();
+ const { isOver, isOverAccepted, files: [file] = [] } = Media.useDropzoneState();
return (