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
11 changes: 11 additions & 0 deletions apps/web/app/api/auth/demo/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getAuth } from '@/lib/auth';
import { getDBService } from '@/lib/database';
import { proxyAuthRequest, shouldProxyAuthRequest } from '@/lib/auth/auth-proxy';
import { ensureDemoConnection } from '@/lib/demo/ensure-demo-connection';
import { NextRequest, NextResponse } from 'next/server';

export const runtime = 'nodejs';
Expand Down Expand Up @@ -158,6 +159,16 @@ export async function POST(req: NextRequest) {
});
}

// Ensure demo SQLite connection exists for the user's organization
const memberships = await db.organizations.listByUser(userId);
if (memberships.length > 0) {
try {
await ensureDemoConnection(db, userId, memberships[0]!.organizationId);
} catch (error) {
console.warn('[demo] failed to ensure demo connection:', error);
}
}

const response = await auth.api.signInEmail({
headers: req.headers,
body: {
Expand Down
13 changes: 12 additions & 1 deletion apps/web/app/api/connection/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { handleApiError } from '../utils/handle-error';
import { parseJsonBody } from '../utils/parse-json';
import { withManagedOrganizationHandler, withOrganizationHandler } from '../utils/with-organization-handler';
import { getApiLocale, translateApi } from '@/app/api/utils/i18n';
import { ensureDemoConnection } from '@/lib/demo/ensure-demo-connection';

// GET /api/connections?id=xxx
export const GET = withOrganizationHandler(async ({ req, db, organizationId }) => {
Expand All @@ -27,7 +28,17 @@ export const GET = withOrganizationHandler(async ({ req, db, organizationId }) =
return NextResponse.json(ResponseUtil.success(record));
}

const data = await db.connections.list(organizationId);
let data = await db.connections.list(organizationId);

if (data.length === 0 && userId) {
try {
await ensureDemoConnection(db, userId, organizationId);
data = await db.connections.list(organizationId);
} catch (err) {
console.warn('[api/connection] failed to create demo connection:', err);
}
}

return NextResponse.json(ResponseUtil.success(data));
} catch (err: any) {
return handleApiError(err);
Expand Down
57 changes: 57 additions & 0 deletions apps/web/lib/demo/ensure-demo-connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import fs from 'node:fs';
import type { DBService } from '@/lib/database';
import { getDemoSqlitePath } from './paths';

const DEMO_CONNECTION_NAME = 'Demo Database';

/**
* Ensure a "Demo Database" SQLite connection exists for the given organization.
* Idempotent: skips if the connection already exists or if the demo.sqlite file is not available.
*/
export async function ensureDemoConnection(
db: DBService,
userId: string,
organizationId: string,
): Promise<void> {
const demoPath = getDemoSqlitePath();
if (!demoPath || !fs.existsSync(demoPath)) {
console.log('[demo] demo.sqlite not found, skipping demo connection creation');
return;
}

const existing = await db.connections.list(organizationId);
const hasDemoConnection = existing.some((c) => c.name === DEMO_CONNECTION_NAME);
if (hasDemoConnection) {
return;
}

console.log(`[demo] creating "${DEMO_CONNECTION_NAME}" connection for org ${organizationId}`);

await db.connections.create(userId, organizationId, {
connection: {
organizationId,
type: 'sqlite',
engine: 'sqlite',
name: DEMO_CONNECTION_NAME,
description: 'Built-in demo database with sample users, orders, and logs data',
host: null,
port: null,
database: 'main',
path: demoPath,
status: 'ready',
},
identities: [
{
name: 'Default',
username: 'sqlite',
isDefault: true,
database: 'main',
enabled: true,
role: null,
options: '{}',
} as any,
],
});

console.log(`[demo] "${DEMO_CONNECTION_NAME}" connection created`);
}
173 changes: 173 additions & 0 deletions apps/web/lib/demo/generate-demo-sqlite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import fs from 'node:fs';
import Database from 'better-sqlite3';
import { faker } from '@faker-js/faker';

const SEED = 42;
const USER_COUNT = 100;
const ORDER_COUNT = 1000;
const LOG_COUNT = 10000;

const ORDER_STATUSES = ['pending', 'completed', 'cancelled', 'refunded'] as const;
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'] as const;
const SERVICES = ['api-gateway', 'auth-service', 'payment-service', 'order-service', 'notification-service', 'search-service', 'analytics-service', 'file-service'] as const;

const LOG_MESSAGES: Record<string, string[]> = {
'debug': [
'Cache miss for key',
'Retry attempt',
'Connection pool stats',
'Query plan analysis complete',
'GC pause detected',
],
'info': [
'Request processed successfully',
'User session created',
'Payment processed',
'Email sent successfully',
'Database migration applied',
'Health check passed',
'New user registered',
],
'warn': [
'Response time exceeded threshold',
'Rate limit approaching',
'Disk usage above 80%',
'Deprecated API version used',
'Connection pool nearly exhausted',
'Certificate expires in 7 days',
],
'error': [
'Database connection timeout',
'Authentication failed',
'Payment gateway unavailable',
'File upload failed: size limit exceeded',
'Unhandled exception in request handler',
'Failed to send notification',
],
};

function createTables(db: Database.Database) {
db.exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
country TEXT NOT NULL,
created_at TEXT NOT NULL
);

CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
amount REAL NOT NULL,
status TEXT NOT NULL,
created_at TEXT NOT NULL
);

CREATE TABLE logs (
id INTEGER PRIMARY KEY,
timestamp TEXT NOT NULL,
level TEXT NOT NULL,
service TEXT NOT NULL,
message TEXT NOT NULL,
duration_ms INTEGER
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_logs_level ON logs(level);
CREATE INDEX idx_logs_timestamp ON logs(timestamp);
CREATE INDEX idx_logs_service ON logs(service);
`);
}

function seedUsers(db: Database.Database) {
const insert = db.prepare('INSERT INTO users (id, name, country, created_at) VALUES (?, ?, ?, ?)');
const tx = db.transaction(() => {
for (let i = 1; i <= USER_COUNT; i++) {
insert.run(
i,
faker.person.fullName(),
faker.location.country(),
faker.date.between({ from: '2024-01-01', to: '2026-03-01' }).toISOString(),
);
}
});
tx();
}

function seedOrders(db: Database.Database) {
const insert = db.prepare('INSERT INTO orders (order_id, user_id, amount, status, created_at) VALUES (?, ?, ?, ?, ?)');
const tx = db.transaction(() => {
for (let i = 1; i <= ORDER_COUNT; i++) {
const userId = faker.number.int({ min: 1, max: USER_COUNT });
const amount = Math.round(faker.number.float({ min: 5, max: 500, fractionDigits: 2 }) * 100) / 100;
const status = faker.helpers.arrayElement(ORDER_STATUSES);
insert.run(
i,
userId,
amount,
status,
faker.date.between({ from: '2025-01-01', to: '2026-03-30' }).toISOString(),
);
}
});
tx();
}

function seedLogs(db: Database.Database) {
const insert = db.prepare('INSERT INTO logs (id, timestamp, level, service, message, duration_ms) VALUES (?, ?, ?, ?, ?, ?)');
// Weighted log levels: more info/debug, fewer warn/error
const levelWeights = { debug: 25, info: 45, warn: 20, error: 10 };
const weightedLevels: string[] = [];
for (const [level, weight] of Object.entries(levelWeights)) {
for (let i = 0; i < weight; i++) weightedLevels.push(level);
}

const tx = db.transaction(() => {
for (let i = 1; i <= LOG_COUNT; i++) {
const level = faker.helpers.arrayElement(weightedLevels);
const service = faker.helpers.arrayElement(SERVICES);
const message = faker.helpers.arrayElement(LOG_MESSAGES[level] ?? LOG_MESSAGES['info']!);
const durationMs = level === 'error'
? faker.number.int({ min: 500, max: 30000 })
: faker.number.int({ min: 1, max: 2000 });
insert.run(
i,
faker.date.between({ from: '2026-03-01', to: '2026-03-31' }).toISOString(),
level,
service,
message,
durationMs,
);
}
});
tx();
}

/**
* Generate the demo.sqlite file at the given absolute path.
* Idempotent: skips if the file already exists.
* Returns true if the file was created, false if it already existed.
*/
export function generateDemoSqlite(targetPath: string): boolean {
if (fs.existsSync(targetPath)) {
console.log(`[demo] demo.sqlite already exists at ${targetPath}, skipping`);
return false;
}

console.log(`[demo] generating demo.sqlite at ${targetPath}...`);
faker.seed(SEED);

const db = new Database(targetPath);
try {
db.pragma('journal_mode = WAL');
createTables(db);
seedUsers(db);
seedOrders(db);
seedLogs(db);
console.log(`[demo] demo.sqlite created (${USER_COUNT} users, ${ORDER_COUNT} orders, ${LOG_COUNT} logs)`);
return true;
} finally {
db.close();
}
}
28 changes: 28 additions & 0 deletions apps/web/lib/demo/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const DEMO_SQLITE_FILENAME = 'demo.sqlite';

/**
* Resolve the absolute path for demo.sqlite based on PGLITE_DB_PATH.
* The demo file lives as a sibling of the PGlite data directory.
*
* Example: PGLITE_DB_PATH = "file:///app/data/dory" → "/app/data/demo.sqlite"
*/
export function resolveDemoSqlitePath(): string {
const raw = process.env.PGLITE_DB_PATH;
if (!raw) {
throw new Error('[demo] PGLITE_DB_PATH is not set, cannot resolve demo.sqlite path');
}

const fsPath = raw.startsWith('file:') ? fileURLToPath(raw) : decodeURIComponent(raw);
const parentDir = path.dirname(path.resolve(fsPath));
return path.join(parentDir, DEMO_SQLITE_FILENAME);
}

/**
* Get the demo.sqlite path from the environment variable set during bootstrap.
*/
export function getDemoSqlitePath(): string | undefined {
return process.env.DEMO_SQLITE_PATH || undefined;
}
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@electric-sql/pglite": "^0.2.17",
"@faker-js/faker": "^10.4.0",
"@hookform/resolvers": "^5.2.2",
"@monaco-editor/react": "^4.7.0",
"@paper-design/shaders-react": "^0.0.71",
Expand Down
15 changes: 15 additions & 0 deletions apps/web/scripts/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { getDatabaseProvider } from '../lib/database/provider';
import { DEFAULT_PGLITE_DB_PATH, DESKTOP_PGLITE_DB_PATH } from '@/shared/data/app.data';
import { ensureFileUrl } from '@/lib/database/pglite/url';
import { isDesktopRuntime } from '@/lib/runtime/runtime';
import { resolveDemoSqlitePath } from '@/lib/demo/paths';
import { generateDemoSqlite } from '@/lib/demo/generate-demo-sqlite';


async function ensureDirForFile(filePath: string) {
Expand Down Expand Up @@ -37,6 +39,17 @@ async function bootstrapPglite() {
await migratePgliteDB();
}

function bootstrapDemoSqlite() {
try {
const demoPath = resolveDemoSqlitePath();
generateDemoSqlite(demoPath);
process.env.DEMO_SQLITE_PATH = demoPath;
console.log('[bootstrap] DEMO_SQLITE_PATH =', demoPath);
} catch (error) {
console.warn('[bootstrap] skipping demo sqlite:', error);
}
}

export async function bootstrap() {
const dbType = getDatabaseProvider();
console.log('[bootstrap] DB_TYPE =', dbType);
Expand All @@ -46,6 +59,8 @@ export async function bootstrap() {
} else {
console.log('[bootstrap] skip bootstrap');
}

bootstrapDemoSqlite();
}

bootstrap().catch(err => {
Expand Down
15 changes: 15 additions & 0 deletions apps/web/scripts/dev-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import path from 'node:path';
import { migratePgliteDB } from '@/lib/database/pglite/migrate-pglite';
import { getDatabaseProvider } from '@/lib/database/provider';
import { ensureFileUrl, extractFilePath } from '@/lib/database/pglite/url';
import { resolveDemoSqlitePath } from '@/lib/demo/paths';
import { generateDemoSqlite } from '@/lib/demo/generate-demo-sqlite';

async function ensureDirForFile(filePath: string) {
const dir = path.dirname(filePath);
Expand Down Expand Up @@ -33,6 +35,17 @@ async function bootstrapPglite() {
await migratePgliteDB();
}

function bootstrapDemoSqlite() {
try {
const demoPath = resolveDemoSqlitePath();
generateDemoSqlite(demoPath);
process.env.DEMO_SQLITE_PATH = demoPath;
console.log('[dev] DEMO_SQLITE_PATH =', demoPath);
} catch (error) {
console.warn('[dev] skipping demo sqlite:', error);
}
}

export async function bootstrapLocalDev() {
const dbType = getDatabaseProvider();

Expand All @@ -44,6 +57,8 @@ export async function bootstrapLocalDev() {
// Other types (postgres / mysql) usually don't need bootstrap in dev
console.log('[dev] skip dev bootstrap');
}

bootstrapDemoSqlite();
}

// Allow direct tsx execution
Expand Down
Loading
Loading