Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ jobs:
run: pnpm --filter=dev-playground exec playwright install --with-deps chromium
- name: Build packages
run: pnpm build
# The generated database registry must reach appkit-ui's built declarations.
- name: Typecheck database components
run: pnpm --filter=dev-playground typecheck:database
- name: Run Integration Tests
run: pnpm --filter=dev-playground test:integration
env:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DatabaseApiError, databaseApi } from "@databricks/appkit-ui/js/beta";
import {
Badge,
Button,
Expand All @@ -17,21 +18,21 @@ import { useCallback, useEffect, useId, useState } from "react";
* is the row an `afterCreate` hook commits alongside each note.
*/

interface Note {
id: number;
board_id: number;
author: string;
body: string;
created_at: string;
}
/** Only a short note preview is needed for the board picker. */
const listBoards = () =>
databaseApi.list("boards", { include: { notes: { limit: 5 } } });

interface Board {
id: number;
slug: string;
title: string;
created_at: string;
notes?: Note[];
}
/** Listing notes directly is what puts them through the entity's serializer. */
const listNotes = (boardId: number) =>
databaseApi.list("notes", {
where: { board_id: boardId },
order: { created_at: "desc" },
limit: 5,
});

// Row types come from the generated schema through the calls that read them.
type Board = Awaited<ReturnType<typeof listBoards>>["items"][number];
type Note = Awaited<ReturnType<typeof listNotes>>["items"][number];

interface NoteEvent {
id: number;
Expand All @@ -44,23 +45,10 @@ interface TimelineNote extends Note {
note_events?: NoteEvent[];
}

interface Timeline extends Board {
interface Timeline extends Omit<Board, "notes"> {
notes?: TimelineNote[];
}

/** Only a short note preview is needed for the board picker. */
const BOARDS_URL = `/api/database/boards?include=${encodeURIComponent(
JSON.stringify({ notes: { limit: 5 } }),
)}`;

/** Listing notes directly is what puts them through the entity's serializer. */
const notesUrl = (boardId: number) =>
`/api/database/notes?where=${encodeURIComponent(
JSON.stringify({ board_id: boardId }),
)}&order=${encodeURIComponent(
JSON.stringify({ created_at: "desc" }),
)}&limit=5`;

/** The audit trail is a read-only include on the generated board detail route. */
const timelineUrl = (boardId: number) =>
`/api/database/boards/${boardId}?include=${encodeURIComponent(
Expand Down Expand Up @@ -89,6 +77,14 @@ async function getJson<T>(url: string): Promise<T> {
return body as T;
}

/** The client decodes the same envelope; a field detail still reads first. */
function errorText(err: unknown): string {
if (err instanceof DatabaseApiError) {
return err.details[0]?.message ?? err.message;
}
return err instanceof Error ? err.message : String(err);
}

export function BoardExplorer() {
const authorFieldId = useId();
const bodyFieldId = useId();
Expand All @@ -108,7 +104,7 @@ export function BoardExplorer() {
const load = useCallback(async (slug?: string | null) => {
setError(null);
try {
const page = await getJson<{ items: Board[] }>(BOARDS_URL);
const page = await listBoards();
setBoards(page.items);
const active =
page.items.find((entry) => entry.slug === slug) ?? page.items[0];
Expand All @@ -120,13 +116,13 @@ export function BoardExplorer() {
return;
}
const [listed, board] = await Promise.all([
getJson<{ items: Note[] }>(notesUrl(active.id)),
listNotes(active.id),
getJson<Timeline>(timelineUrl(active.id)),
]);
setNotes(listed.items);
setTimeline(board);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setError(errorText(err));
}
}, []);

Expand Down Expand Up @@ -222,7 +218,7 @@ export function BoardExplorer() {
variant="secondary"
className="ml-2 tabular-nums font-normal"
>
{entry.notes?.length ?? 0} notes
{entry.notes.length} notes
</Badge>
</Button>
))}
Expand Down
15 changes: 15 additions & 0 deletions apps/dev-playground/client/tsconfig.database.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// Type-check the database components against the built appkit-ui
// declarations, where the Vite alias points, so the generated registry is
// proven to reach `@databricks/appkit-ui/js/beta` through `dist`. Run
// `pnpm build` first.
"extends": "./tsconfig.app.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.database.tsbuildinfo",
"paths": {
"@/*": ["./src/*"],
"@databricks/appkit-ui/*": ["../../../packages/appkit-ui/dist/*"]
}
},
"include": ["src/components/database", "../shared/appkit-types/database.d.ts"]
}
1 change: 1 addition & 0 deletions apps/dev-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"install": "cd client && npm install && cd ..",
"preview": "vite preview",
"check": "tsc",
"typecheck:database": "tsc -p client/tsconfig.database.json --noEmit",
"clean": "rm -rf build && cd client && rm -rf dist",
"clean:full": "rm -rf build node_modules && cd client && rm -rf dist node_modules",
"test:integration": "playwright test",
Expand Down
128 changes: 97 additions & 31 deletions apps/dev-playground/shared/appkit-types/database.d.ts
Original file line number Diff line number Diff line change
@@ -1,124 +1,190 @@
// Auto-generated by AppKit - DO NOT EDIT
import "@databricks/appkit";

declare module "@databricks/appkit" {
type DatabaseLogicalFilter<T> = T & {
and?: readonly DatabaseLogicalFilter<T>[];
or?: readonly DatabaseLogicalFilter<T>[];
};
type DatabaseLogicalFilter<T> = T & {
and?: readonly DatabaseLogicalFilter<T>[];
or?: readonly DatabaseLogicalFilter<T>[];
};

interface DatabaseRegistry {
"boards": {
row: {
interface GeneratedDatabaseRegistry {
"boards": {
row: {
"id": number;
"slug": string;
"title": string;
"created_at": string;
};
publicRow: {
publicRow: {
"id": number;
"slug": string;
"title": string;
"created_at": string;
};
insert: {
insert: {
"slug": string;
"title": string;
"created_at"?: string;
};
update: {
update: {
"slug"?: string;
"title"?: string;
"created_at"?: string;
};
filters: DatabaseLogicalFilter<{
filters: DatabaseLogicalFilter<{
"id"?: number | readonly (number)[] | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"slug"?: string | readonly (string)[] | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"title"?: string | readonly (string)[] | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"created_at"?: string | readonly (string)[] | { eq?: string; neq?: string; in?: readonly (string)[]; gt?: string; gte?: string; lt?: string; lte?: string; };
}>;
includes: {
includes: {
"notes": { to: "notes"; many: true };
};
hasPrimaryKey: true;
hasPrimaryKey: true;
api: {
insert: {
"slug": string;
"title": string;
"created_at"?: string;
};
update: {
"slug"?: string;
"title"?: string;
};
filters: DatabaseLogicalFilter<{
"id"?: number | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"slug"?: string | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"title"?: string | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"created_at"?: string | { eq?: string; neq?: string; in?: readonly (string)[]; gt?: string; gte?: string; lt?: string; lte?: string; };
}>;
orderable: "id" | "slug" | "title" | "created_at";
key: "id";
};
"notes": {
row: {
};
"notes": {
row: {
"id": number;
"board_id": number;
"author": string;
"author_email": string | null;
"body": string;
"created_at": string;
};
publicRow: {
publicRow: {
"id": number;
"board_id": number;
"author": string;
"body": string;
"created_at": string;
};
insert: {
insert: {
"board_id": number;
"author": string;
"author_email"?: string | null;
"body": string;
"created_at"?: string;
};
update: {
update: {
"board_id"?: number;
"author"?: string;
"author_email"?: string | null;
"body"?: string;
"created_at"?: string;
};
filters: DatabaseLogicalFilter<{
filters: DatabaseLogicalFilter<{
"id"?: number | readonly (number)[] | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"board_id"?: number | readonly (number)[] | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"author"?: string | readonly (string)[] | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"author_email"?: string | readonly (string)[] | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; is?: null; };
"body"?: string | readonly (string)[] | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"created_at"?: string | readonly (string)[] | { eq?: string; neq?: string; in?: readonly (string)[]; gt?: string; gte?: string; lt?: string; lte?: string; };
}>;
includes: {
includes: {
"boards": { to: "boards"; many: false };
"note_events": { to: "note_events"; many: true };
};
hasPrimaryKey: true;
hasPrimaryKey: true;
api: {
insert: {
"board_id": number;
"author": string;
"body": string;
"created_at"?: string;
};
update: {
"board_id"?: number;
"author"?: string;
"body"?: string;
};
filters: DatabaseLogicalFilter<{
"id"?: number | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"board_id"?: number | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"author"?: string | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"body"?: string | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"created_at"?: string | { eq?: string; neq?: string; in?: readonly (string)[]; gt?: string; gte?: string; lt?: string; lte?: string; };
}>;
orderable: "id" | "board_id" | "author" | "body" | "created_at";
key: "id";
};
"note_events": {
row: {
};
"note_events": {
row: {
"id": number;
"note_id": number;
"action": string;
"created_at": string;
};
publicRow: {
publicRow: {
"id": number;
"note_id": number;
"action": string;
"created_at": string;
};
insert: {
insert: {
"note_id": number;
"action": string;
"created_at"?: string;
};
update: {
update: {
"note_id"?: number;
"action"?: string;
"created_at"?: string;
};
filters: DatabaseLogicalFilter<{
filters: DatabaseLogicalFilter<{
"id"?: number | readonly (number)[] | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"note_id"?: number | readonly (number)[] | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"action"?: string | readonly (string)[] | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"created_at"?: string | readonly (string)[] | { eq?: string; neq?: string; in?: readonly (string)[]; gt?: string; gte?: string; lt?: string; lte?: string; };
}>;
includes: {
includes: {
"notes": { to: "notes"; many: false };
};
hasPrimaryKey: true;
hasPrimaryKey: true;
api: {
insert: {
"note_id": number;
"action": string;
"created_at"?: string;
};
update: {
"note_id"?: number;
"action"?: string;
};
filters: DatabaseLogicalFilter<{
"id"?: number | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"note_id"?: number | { eq?: number; neq?: number; in?: readonly (number)[]; gt?: number; gte?: number; lt?: number; lte?: number; };
"action"?: string | { eq?: string; neq?: string; in?: readonly (string)[]; like?: string; ilike?: string; };
"created_at"?: string | { eq?: string; neq?: string; in?: readonly (string)[]; gt?: string; gte?: string; lt?: string; lte?: string; };
}>;
orderable: "id" | "note_id" | "action" | "created_at";
key: "id";
};
}
};
}

declare module "@databricks/appkit" {
interface DatabaseRegistry extends GeneratedDatabaseRegistry {}
}

declare global {
interface DatabricksAppKitDatabaseRegistry extends GeneratedDatabaseRegistry {}
}
Loading
Loading