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 (