Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Badge } from "@databricks/appkit-ui/react";

/**
* The create transaction and the separate read serializer. Only database
* writes through the transaction-bound client share the mutation's rollback.
*/

interface Step {
name: string;
kind: "async" | "sql" | "sync";
detail: string;
}

const IN_TRANSACTION: Step[] = [
{
name: "beforeCreate(values, ctx)",
kind: "async",
detail:
"Calls the redaction agent with a 10-second cancellation signal, rejects failed or blank output, and stamps the private author_email.",
},
{
name: "INSERT",
kind: "sql",
detail: "The row the caller asked for, plus whatever the hook added.",
},
{
name: "afterCreate(row, ctx)",
kind: "async",
detail:
"Sees the persisted row. Writes through ctx.app.database join this transaction — here, the note_events entry.",
},
];

const KIND_LABEL: Record<Step["kind"], string> = {
async: "async",
sql: "sql",
sync: "sync",
};

function StepRow({ step }: { step: Step }) {
return (
<div className="flex items-start gap-3 py-2">
<Badge
variant={step.kind === "sql" ? "secondary" : "outline"}
className="shrink-0 w-14 justify-center text-[10px]"
>
{KIND_LABEL[step.kind]}
</Badge>
<div className="min-w-0">
<code className="text-xs font-semibold">{step.name}</code>
<p className="text-xs text-muted-foreground mt-0.5">{step.detail}</p>
</div>
</div>
);
}

export function HookLifecycle() {
return (
<div className="space-y-3">
<code className="text-xs font-semibold">POST /api/database/notes</code>

<div className="rounded-md border-2 border-dashed p-3">
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
one transaction
</span>
<span className="text-xs text-muted-foreground">
a throw anywhere rolls back everything below
</span>
</div>
<div className="divide-y">
{IN_TRANSACTION.map((step) => (
<StepRow key={step.name} step={step} />
))}
</div>
</div>

<div className="rounded-md border p-3">
<div className="mb-1">
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
on a subsequent read
</span>
</div>
<StepRow
step={{
name: "serialize(row, { operation })",
kind: "sync",
detail:
"Runs on the projected public row. Its type returns a value, not a promise, so the read path cannot grow a network call — anything derived has to already be on the row.",
}}
/>
</div>

<p className="text-xs text-muted-foreground">
<code>ctx.app.database</code> provides the transaction-bound database
client. Import other APIs separately and give network calls their own
cancellation signal: the database deadline does not cancel them, and
rollback does not undo their external effects.
</p>
</div>
);
}
3 changes: 3 additions & 0 deletions apps/dev-playground/client/src/components/database/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { BoardExplorer } from "./board-explorer";
export { HookLifecycle } from "./hook-lifecycle";
export { RefusalProbe } from "./refusal-probe";
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Badge, Button } from "@databricks/appkit-ui/react";
import { PlayIcon } from "lucide-react";
import { useState } from "react";

/**
* Fires one request the plugin is expected to refuse and prints what came
* back. The guarantees on this page are only worth as much as the response,
* so the page asks the running server instead of asserting.
*/

interface Attempt {
status: number;
body: string;
}

export function RefusalProbe({
label,
request,
send,
}: {
/** What the caller is trying to get away with. */
label: string;
/** The request as a reader would write it, shown before running. */
request: string;
send: () => Promise<Response>;
}) {
const [attempt, setAttempt] = useState<Attempt | null>(null);
const [running, setRunning] = useState(false);

const run = async () => {
setRunning(true);
try {
const response = await send();
const text = await response.text();
setAttempt({ status: response.status, body: text.slice(0, 400) });
} catch (error) {
setAttempt({ status: 0, body: String(error) });
} finally {
setRunning(false);
}
};

return (
<div className="rounded-md border p-3 space-y-2">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-medium">{label}</p>
<code className="text-xs text-muted-foreground break-all">
{request}
</code>
</div>
<Button size="sm" variant="outline" onClick={run} disabled={running}>
<PlayIcon className="h-3 w-3" />
Try it
</Button>
</div>
{attempt && (
<div className="flex items-start gap-2">
<Badge
variant={attempt.status >= 400 ? "destructive" : "secondary"}
className="tabular-nums shrink-0"
>
{attempt.status}
</Badge>
<code className="text-xs break-all">{attempt.body}</code>
</div>
)}
</div>
);
}
8 changes: 8 additions & 0 deletions apps/dev-playground/client/src/lib/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
BarChart3Icon,
BotIcon,
DatabaseIcon,
DatabaseZapIcon,
FileCode2Icon,
FolderIcon,
GaugeIcon,
Expand Down Expand Up @@ -59,6 +60,13 @@ export const NAV_GROUPS: ReadonlyArray<NavGroup> = [
"Query execution, charts, and interactive components against live SQL.",
icon: BarChart3Icon,
},
{
to: "/database",
label: "Database",
description:
"Declare a Postgres schema and get typed entities, generated CRUD routes, and transactional hooks.",
icon: DatabaseZapIcon,
},
{
to: "/arrow-analytics",
label: "Arrow Analytics",
Expand Down
21 changes: 21 additions & 0 deletions apps/dev-playground/client/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Route as LakebaseRouteRouteImport } from './routes/lakebase.route'
import { Route as JobsRouteRouteImport } from './routes/jobs.route'
import { Route as GenieRouteRouteImport } from './routes/genie.route'
import { Route as FilesRouteRouteImport } from './routes/files.route'
import { Route as DatabaseRouteRouteImport } from './routes/database.route'
import { Route as DataVisualizationRouteRouteImport } from './routes/data-visualization.route'
import { Route as ChartInferenceRouteRouteImport } from './routes/chart-inference.route'
import { Route as ArrowAnalyticsRouteRouteImport } from './routes/arrow-analytics.route'
Expand Down Expand Up @@ -101,6 +102,11 @@ const FilesRouteRoute = FilesRouteRouteImport.update({
path: '/files',
getParentRoute: () => rootRouteImport,
} as any)
const DatabaseRouteRoute = DatabaseRouteRouteImport.update({
id: '/database',
path: '/database',
getParentRoute: () => rootRouteImport,
} as any)
const DataVisualizationRouteRoute = DataVisualizationRouteRouteImport.update({
id: '/data-visualization',
path: '/data-visualization',
Expand Down Expand Up @@ -145,6 +151,7 @@ export interface FileRoutesByFullPath {
'/arrow-analytics': typeof ArrowAnalyticsRouteRoute
'/chart-inference': typeof ChartInferenceRouteRoute
'/data-visualization': typeof DataVisualizationRouteRoute
'/database': typeof DatabaseRouteRoute
'/files': typeof FilesRouteRoute
'/genie': typeof GenieRouteRoute
'/jobs': typeof JobsRouteRoute
Expand All @@ -168,6 +175,7 @@ export interface FileRoutesByTo {
'/arrow-analytics': typeof ArrowAnalyticsRouteRoute
'/chart-inference': typeof ChartInferenceRouteRoute
'/data-visualization': typeof DataVisualizationRouteRoute
'/database': typeof DatabaseRouteRoute
'/files': typeof FilesRouteRoute
'/genie': typeof GenieRouteRoute
'/jobs': typeof JobsRouteRoute
Expand All @@ -192,6 +200,7 @@ export interface FileRoutesById {
'/arrow-analytics': typeof ArrowAnalyticsRouteRoute
'/chart-inference': typeof ChartInferenceRouteRoute
'/data-visualization': typeof DataVisualizationRouteRoute
'/database': typeof DatabaseRouteRoute
'/files': typeof FilesRouteRoute
'/genie': typeof GenieRouteRoute
'/jobs': typeof JobsRouteRoute
Expand All @@ -217,6 +226,7 @@ export interface FileRouteTypes {
| '/arrow-analytics'
| '/chart-inference'
| '/data-visualization'
| '/database'
| '/files'
| '/genie'
| '/jobs'
Expand All @@ -240,6 +250,7 @@ export interface FileRouteTypes {
| '/arrow-analytics'
| '/chart-inference'
| '/data-visualization'
| '/database'
| '/files'
| '/genie'
| '/jobs'
Expand All @@ -263,6 +274,7 @@ export interface FileRouteTypes {
| '/arrow-analytics'
| '/chart-inference'
| '/data-visualization'
| '/database'
| '/files'
| '/genie'
| '/jobs'
Expand All @@ -287,6 +299,7 @@ export interface RootRouteChildren {
ArrowAnalyticsRouteRoute: typeof ArrowAnalyticsRouteRoute
ChartInferenceRouteRoute: typeof ChartInferenceRouteRoute
DataVisualizationRouteRoute: typeof DataVisualizationRouteRoute
DatabaseRouteRoute: typeof DatabaseRouteRoute
FilesRouteRoute: typeof FilesRouteRoute
GenieRouteRoute: typeof GenieRouteRoute
JobsRouteRoute: typeof JobsRouteRoute
Expand Down Expand Up @@ -403,6 +416,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof FilesRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/database': {
id: '/database'
path: '/database'
fullPath: '/database'
preLoaderRoute: typeof DatabaseRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/data-visualization': {
id: '/data-visualization'
path: '/data-visualization'
Expand Down Expand Up @@ -463,6 +483,7 @@ const rootRouteChildren: RootRouteChildren = {
ArrowAnalyticsRouteRoute: ArrowAnalyticsRouteRoute,
ChartInferenceRouteRoute: ChartInferenceRouteRoute,
DataVisualizationRouteRoute: DataVisualizationRouteRoute,
DatabaseRouteRoute: DatabaseRouteRoute,
FilesRouteRoute: FilesRouteRoute,
GenieRouteRoute: GenieRouteRoute,
JobsRouteRoute: JobsRouteRoute,
Expand Down
Loading
Loading