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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ coverage
out/
build
dist
*.tsbuildinfo

# misc
.DS_Store
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ Check the example apps to see common usages or use them as a boilerplate.
# Packages

- [ra-data-simple-prisma](./packages/ra-data-simple-prisma/)
- [ra-data-duckdb](./packages/ra-data-duckdb/)
- [next-auth-prisma-adapter](./packages/next-auth-prisma-adapter/)

# Examples

[example admin app](./apps/admin/) Debug, test, and develop the packages, but also use it as the admin/CMS for the website!
[example website](./apps/website/) A nextjs/mui boilerplate to show the data (very much under construction yet)
[DuckDB admin example](./apps/duckdb-admin/) Minimal in-memory DuckDB and React Admin app.

### Development

Expand Down
16 changes: 16 additions & 0 deletions apps/duckdb-admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# DuckDB Admin Example

A minimal Next.js app using React Admin and the workspace's
`ra-data-duckdb` package.

The API creates an in-memory DuckDB database with three users when the server
starts. Changes persist until the development server restarts.

```sh
pnpm --filter duckdb-admin-example dev
```

Open [http://localhost:3030](http://localhost:3030).

Scripts pass `NODE_OPTIONS=--no-webstorage` because Node.js 25+ ships a partial
`localStorage` that breaks Next.js 15.3 SSR.
10 changes: 10 additions & 0 deletions apps/duckdb-admin/app/Admin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

import dynamic from "next/dynamic";

const AdminApp = dynamic(() => import("./AdminApp"), {
ssr: false,
loading: () => <p>Loading admin…</p>,
});

export default AdminApp;
73 changes: 73 additions & 0 deletions apps/duckdb-admin/app/AdminApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use client";

import {
Admin,
BooleanField,
BooleanInput,
Create,
Datagrid,
Edit,
EditButton,
List,
NumberField,
NumberInput,
Resource,
SimpleForm,
TextField,
TextInput,
required,
} from "react-admin";
import { dataProvider } from "ra-data-duckdb";

const usersProvider = dataProvider("/api");

const userFilters = [
<TextInput key="q" source="q" label="Search" alwaysOn />,
];

const UserList = () => (
<List filters={userFilters}>
<Datagrid>
<TextField source="id" />
<TextField source="name" />
<TextField source="email" />
<NumberField source="age" />
<BooleanField source="active" />
<EditButton />
</Datagrid>
</List>
);

const UserForm = () => (
<SimpleForm>
<TextInput source="name" validate={required()} />
<TextInput source="email" validate={required()} />
<NumberInput source="age" />
<BooleanInput source="active" />
</SimpleForm>
);

const UserCreate = () => (
<Create>
<UserForm />
</Create>
);

const UserEdit = () => (
<Edit>
<UserForm />
</Edit>
);

const AdminApp = () => (
<Admin dataProvider={usersProvider} disableTelemetry>
<Resource
name="users"
list={UserList}
create={UserCreate}
edit={UserEdit}
/>
</Admin>
);

export default AdminApp;
63 changes: 63 additions & 0 deletions apps/duckdb-admin/app/api/[resource]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { DuckDBInstance } from "@duckdb/node-api";
import {
defaultHandler,
DuckDBExecutor,
fromDuckDBConnection,
NotFoundError,
} from "ra-data-duckdb/server";
import { NextResponse } from "next/server";

export const runtime = "nodejs";

const globalForDuckDB = globalThis as typeof globalThis & {
duckDBDemo?: Promise<DuckDBExecutor>;
};

const createDatabase = async (): Promise<DuckDBExecutor> => {
const instance = await DuckDBInstance.create(":memory:");
const connection = await instance.connect();
const db = fromDuckDBConnection(connection);

await db.run("CREATE SEQUENCE users_id_seq START 4");
await db.run(`
CREATE TABLE users (
id INTEGER PRIMARY KEY DEFAULT nextval('users_id_seq'),
name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
age INTEGER,
active BOOLEAN DEFAULT true
)
`);
await db.run(`
INSERT INTO users (id, name, email, age, active)
VALUES
(1, 'Ada Lovelace', 'ada@example.com', 36, true),
(2, 'Grace Hopper', 'grace@example.com', 85, true),
(3, 'Alan Turing', 'alan@example.com', 41, false)
`);

return db;
};

const getDatabase = () => {
globalForDuckDB.duckDBDemo ??= createDatabase();
return globalForDuckDB.duckDBDemo;
};

const handler = async (request: Request) => {
try {
const payload = await request.json();
const db = await getDatabase();
const result = await defaultHandler(payload, db, {
getList: { searchColumns: ["name", "email"] },
});

return NextResponse.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
const status = error instanceof NotFoundError ? error.status : 400;
return NextResponse.json({ message }, { status });
}
};

export { handler as GET, handler as POST };
15 changes: 15 additions & 0 deletions apps/duckdb-admin/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";

export const metadata: Metadata = {
title: "React Admin + DuckDB",
description: "Minimal ra-data-duckdb example",
};

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body style={{ margin: 0 }}>{children}</body>
</html>
);
}
5 changes: 5 additions & 0 deletions apps/duckdb-admin/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Admin from "./Admin";

export default function HomePage() {
return <Admin />;
}
5 changes: 5 additions & 0 deletions apps/duckdb-admin/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
37 changes: 37 additions & 0 deletions apps/duckdb-admin/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const path = require("path");

/** @type {import('next').NextConfig} */
const nextConfig = {
serverExternalPackages: ["@duckdb/node-api"],
transpilePackages: [
"ra-data-duckdb",
"react-admin",
"ra-core",
"ra-ui-materialui",
"ra-i18n-polyglot",
"ra-language-english",
"@mui/material",
"@mui/icons-material",
"@mui/system",
"@mui/utils",
],
modularizeImports: {
"@mui/icons-material": {
transform: "@mui/icons-material/{{member}}",
},
},
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
// ESM ra-ui-materialui default-imports CJS icon files; without this,
// webpack treats the CJS module namespace as the component (got: object).
"@mui/icons-material": path.resolve(
__dirname,
"../../node_modules/@mui/icons-material/esm"
),
};
return config;
},
};

module.exports = nextConfig;
27 changes: 27 additions & 0 deletions apps/duckdb-admin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "duckdb-admin-example",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "NODE_OPTIONS=--no-webstorage next dev -p 3030",
"build": "NODE_OPTIONS=--no-webstorage next build",
"start": "NODE_OPTIONS=--no-webstorage next start -p 3030"
},
"dependencies": {
"@duckdb/node-api": "1.5.5-r.4",
"@mui/icons-material": "5.17.1",
"@mui/material": "5.17.1",
"next": "15.3.0",
"ra-data-duckdb": "workspace:*",
"react": "19.1.0",
"react-admin": "^5.14.1",
"react-dom": "19.1.0",
"react-hook-form": "^7.65.0"
},
"devDependencies": {
"@types/node": "^22",
"@types/react": "19.1.1",
"@types/react-dom": "19.1.2",
"typescript": "^5.9.3"
}
}
25 changes: 25 additions & 0 deletions apps/duckdb-admin/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}
81 changes: 81 additions & 0 deletions packages/ra-data-duckdb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# React Admin + DuckDB

Simple react-admin data provider backed by [DuckDB](https://duckdb.org/). Same request shape as `ra-data-simple-prisma`: the browser talks HTTP, the server runs SQL.

### Installation

```
pnpm i ra-data-duckdb @duckdb/node-api
```

### Frontend

```tsx
import { Admin, Resource } from "react-admin";
import { dataProvider } from "ra-data-duckdb";

const ReactAdmin = () => (
<Admin dataProvider={dataProvider("/api")}>
<Resource name="users" />
</Admin>
);
```

### Backend (Next.js App Router)

```ts
// app/api/[resource]/route.ts
import { DuckDBInstance } from "@duckdb/node-api";
import { defaultHandler, fromDuckDBConnection } from "ra-data-duckdb/server";
import { NextResponse } from "next/server";

const instance = await DuckDBInstance.create("data.duckdb");
const connection = await instance.connect();
const db = fromDuckDBConnection(connection);

const handler = async (req: Request) => {
const body = await req.json();
const result = await defaultHandler(body, db, {
// optional: map react-admin resource names to table names
resourceToTableMap: { users: "app_users" },
});
return NextResponse.json(result);
};

export { handler as GET, handler as POST };
```

### Custom executor

Handlers take a minimal `DuckDBExecutor` (`all` / `run`). Use `fromDuckDBConnection` for `@duckdb/node-api`, or supply your own:

```ts
import { defaultHandler, DuckDBExecutor } from "ra-data-duckdb/server";

const db: DuckDBExecutor = {
all: async (sql, params) => { /* ... */ },
run: async (sql, params) => { /* ... */ },
};

await defaultHandler(body, db);
```

### Filters

Common react-admin filters are mapped to SQL:

| Filter | SQL |
| --- | --- |
| `age: 30` / `age_eq` | `"age" = $p` |
| `age_gte` / `_gt` / `_lte` / `_lt` | comparison |
| `name: "Al"` (string) | `LIKE '%Al%'` (case-insensitive by default) |
| `id_in: [1,2]` / array value | `IN (...)` |
| `q` + `searchColumns` | OR of LIKE across columns |
| `OR` / `AND` / `NOT` | grouped boolean |

Resource and column names are validated and quoted; only `[A-Za-z_][A-Za-z0-9_]*` identifiers are allowed.

### Notes

- DuckDB is not ideal for high-concurrency multi-writer admin apps; prefer a single writer or an analytics/read-heavy use case.
- Audit logs and Prisma-style relation `include` / `connect` are not ported — keep v1 flat-table CRUD.
Loading