Skip to content

Commit e92d225

Browse files
authored
Merge pull request #131 from dorylab/sqlite-demo
refactor: ingest demo data on org created
2 parents 1de35c1 + ada7468 commit e92d225

File tree

17 files changed

+176
-65
lines changed

17 files changed

+176
-65
lines changed

apps/web/app/api/connection/route.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { handleApiError } from '../utils/handle-error';
55
import { parseJsonBody } from '../utils/parse-json';
66
import { withManagedOrganizationHandler, withOrganizationHandler } from '../utils/with-organization-handler';
77
import { getApiLocale, translateApi } from '@/app/api/utils/i18n';
8-
import { ensureDemoConnection } from '@/lib/demo/ensure-demo-connection';
98

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

31-
let data = await db.connections.list(organizationId);
32-
33-
if (data.length === 0 && userId) {
34-
try {
35-
await ensureDemoConnection(db, userId, organizationId);
36-
data = await db.connections.list(organizationId);
37-
} catch (err) {
38-
console.warn('[api/connection] failed to create demo connection:', err);
39-
}
40-
}
41-
30+
const data = await db.connections.list(organizationId);
4231
return NextResponse.json(ResponseUtil.success(data));
4332
} catch (err: any) {
4433
return handleApiError(err);

apps/web/lib/auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Stripe from 'stripe';
77
import { and, eq, isNull, or } from 'drizzle-orm';
88
import { createCachedAsyncFactory } from '@dory/auth-core';
99
import type { PostgresDBClient } from '../types';
10+
import { getDBService } from './database';
1011
import { getClient } from './database/postgres/client';
1112
import { getDatabaseProvider } from './database/provider';
1213
import { schema } from './database/schema';
@@ -262,6 +263,10 @@ function createAuth() {
262263
},
263264
};
264265
},
266+
afterCreateOrganization: async ({ organization, user }) => {
267+
const dbService = await getDBService();
268+
await dbService.organizations.ensureOrganizationDefaults(user.id, organization.id, dbService.connections);
269+
},
265270
},
266271
sendInvitationEmail: async ({ id, email, role, organization, inviter }) => {
267272
if (!publicAuthBaseUrl) {

apps/web/lib/connection/config-builder.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ConnectionListIdentity, ConnectionListItem, ConnectionSsh, TestConnectionPayload } from '@/types/connections';
2+
import { resolveStoredSqlitePath } from '@/lib/demo/paths';
23
import { UnsupportedTypeError } from './base/errors';
34
import type { BaseConfig } from './base/types';
45
import { applyQueryRequestTimeout } from './defaults';
@@ -99,7 +100,7 @@ export function buildStoredConnectionConfig(
99100
const type = resolveConnectionType(connection.type ?? connection.engine ?? 'clickhouse');
100101

101102
if (type === 'sqlite') {
102-
const normalizedPath = connection.path?.trim();
103+
const normalizedPath = resolveStoredSqlitePath(connection.path);
103104
if (!normalizedPath) {
104105
throw createError('missing_path');
105106
}
@@ -150,7 +151,7 @@ export function buildTestConnectionConfig(
150151
const type = resolveConnectionType(connection.type ?? connection.engine ?? 'clickhouse');
151152

152153
if (type === 'sqlite') {
153-
const normalizedPath = connection.path?.trim();
154+
const normalizedPath = resolveStoredSqlitePath(connection.path);
154155
if (!normalizedPath) {
155156
throw createError('missing_path');
156157
}

apps/web/lib/connection/display.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import type { ConnectionListItem } from '@/types/connections';
2+
import { isDemoSqliteConnectionPath } from '@/lib/demo/paths';
23

34
export function getConnectionLocationLabel(connection?: ConnectionListItem['connection'] | null) {
45
if (!connection) return null;
56

67
if (connection.type === 'sqlite') {
78
const normalizedPath = connection.path?.trim();
9+
if (isDemoSqliteConnectionPath(normalizedPath)) {
10+
return 'Built-in demo.sqlite';
11+
}
812
return normalizedPath || null;
913
}
1014

apps/web/lib/connection/drivers/sqlite/sqlite-driver.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'node:path';
22
import Database from 'better-sqlite3';
33
import { MAX_RESULT_ROWS } from '@/app/config/sql-console';
4+
import { resolveStoredSqlitePath } from '@/lib/demo/paths';
45
import { enforceSelectLimit } from '@/lib/connection/base/limit';
56
import { compileParams } from '@/lib/connection/base/params/compile';
67
import type { DriverQueryParams } from '@/lib/connection/base/params/types';
@@ -70,7 +71,7 @@ function getSqliteVersion(db: SqliteDatabase): string | undefined {
7071
}
7172

7273
export function resolveSqlitePath(config: BaseConfig): string {
73-
return assertAbsolutePath(config.path);
74+
return assertAbsolutePath(resolveStoredSqlitePath(config.path));
7475
}
7576

7677
export function openSqliteDatabase(config: BaseConfig): SqliteDatabase {

apps/web/lib/database/postgres/impl/organization/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { DatabaseError } from '@/lib/errors/DatabaseError';
33
import { PostgresDBClient } from '@/types';
44
import { and, eq, isNull, or } from 'drizzle-orm';
55
import { organizations } from '@/lib/database/schema';
6+
import { ensureDemoConnection } from '@/lib/demo/ensure-demo-connection';
67
import { organizationMembers } from '../../schemas';
78
import { translateDatabase } from '@/lib/database/i18n';
9+
import type { PostgresConnectionsRepository } from '../connections';
810

911
export class PostgresOrganizationsRepository {
1012
private db!: PostgresDBClient;
@@ -24,6 +26,20 @@ export class PostgresOrganizationsRepository {
2426
return this.db.select().from(organizationMembers).where(eq(organizationMembers.userId, userId));
2527
}
2628

29+
async ensureOrganizationDefaults(
30+
userId: string,
31+
organizationId: string,
32+
connectionsRepository: PostgresConnectionsRepository,
33+
) {
34+
return ensureDemoConnection(
35+
{
36+
connections: connectionsRepository,
37+
},
38+
userId,
39+
organizationId,
40+
);
41+
}
42+
2743
async getOrganizationBySlugOrId(value: string) {
2844
const rows = await this.db
2945
.select()
@@ -46,7 +62,7 @@ export class PostgresOrganizationsRepository {
4662
),
4763
)
4864
.limit(1);
49-
65+
5066
return rows.length > 0;
5167
}
5268
}
Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,52 @@
11
import fs from 'node:fs';
22
import type { DBService } from '@/lib/database';
3-
import { getDemoSqlitePath } from './paths';
3+
import { DEMO_SQLITE_CONNECTION_PATH, getDemoSqlitePath } from './paths';
44

55
const DEMO_CONNECTION_NAME = 'Demo Database';
6+
type DemoConnectionService = Pick<DBService, 'connections'>;
7+
export type EnsureDemoConnectionResult = 'created' | 'updated' | 'exists' | 'skipped';
68

79
/**
810
* Ensure a "Demo Database" SQLite connection exists for the given organization.
911
* Idempotent: skips if the connection already exists or if the demo.sqlite file is not available.
1012
*/
1113
export async function ensureDemoConnection(
12-
db: DBService,
14+
db: DemoConnectionService,
1315
userId: string,
1416
organizationId: string,
15-
): Promise<void> {
17+
): Promise<EnsureDemoConnectionResult> {
1618
const demoPath = getDemoSqlitePath();
1719
if (!demoPath || !fs.existsSync(demoPath)) {
1820
console.log('[demo] demo.sqlite not found, skipping demo connection creation');
19-
return;
21+
return 'skipped';
2022
}
2123

2224
const existing = await db.connections.list(organizationId);
23-
const hasDemoConnection = existing.some((c) => c.name === DEMO_CONNECTION_NAME);
24-
if (hasDemoConnection) {
25-
return;
25+
const existingDemoConnection = existing.find(item => item.connection.name === DEMO_CONNECTION_NAME);
26+
if (existingDemoConnection) {
27+
if (existingDemoConnection.connection.path !== DEMO_SQLITE_CONNECTION_PATH) {
28+
await db.connections.update(organizationId, existingDemoConnection.connection.id, {
29+
connection: {
30+
organizationId,
31+
type: 'sqlite',
32+
engine: 'sqlite',
33+
name: existingDemoConnection.connection.name,
34+
description: existingDemoConnection.connection.description ?? undefined,
35+
host: existingDemoConnection.connection.host,
36+
port: existingDemoConnection.connection.port,
37+
httpPort: existingDemoConnection.connection.httpPort ?? undefined,
38+
database: existingDemoConnection.connection.database ?? undefined,
39+
options: existingDemoConnection.connection.options,
40+
status: existingDemoConnection.connection.status,
41+
environment: existingDemoConnection.connection.environment,
42+
tags: existingDemoConnection.connection.tags,
43+
path: DEMO_SQLITE_CONNECTION_PATH,
44+
},
45+
identities: [],
46+
});
47+
return 'updated';
48+
}
49+
return 'exists';
2650
}
2751

2852
console.log(`[demo] creating "${DEMO_CONNECTION_NAME}" connection for org ${organizationId}`);
@@ -37,21 +61,29 @@ export async function ensureDemoConnection(
3761
host: null,
3862
port: null,
3963
database: 'main',
40-
path: demoPath,
41-
status: 'ready',
64+
path: DEMO_SQLITE_CONNECTION_PATH,
4265
},
4366
identities: [
4467
{
68+
id: '',
69+
connectionId: '',
70+
organizationId,
4571
name: 'Default',
4672
username: 'sqlite',
73+
role: undefined,
74+
options: '{}',
4775
isDefault: true,
4876
database: 'main',
4977
enabled: true,
50-
role: null,
51-
options: '{}',
52-
} as any,
78+
status: 'active',
79+
createdAt: new Date(),
80+
updatedAt: new Date(),
81+
deletedAt: null,
82+
password: undefined,
83+
},
5384
],
5485
});
5586

5687
console.log(`[demo] "${DEMO_CONNECTION_NAME}" connection created`);
88+
return 'created';
5789
}

apps/web/lib/demo/paths.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
11
import path from 'node:path';
2-
import { fileURLToPath } from 'node:url';
32

43
const DEMO_SQLITE_FILENAME = 'demo.sqlite';
4+
const DEMO_SQLITE_DIR = path.join('public', 'resources');
5+
const DEMO_SQLITE_RELATIVE_PATH = path.join(DEMO_SQLITE_DIR, DEMO_SQLITE_FILENAME);
6+
export const DEMO_SQLITE_CONNECTION_PATH = 'dory://demo-sqlite';
57

68
/**
7-
* Resolve the absolute path for demo.sqlite based on PGLITE_DB_PATH.
8-
* The demo file lives as a sibling of the PGlite data directory.
9-
*
10-
* Example: PGLITE_DB_PATH = "file:///app/data/dory" → "/app/data/demo.sqlite"
9+
* Resolve the absolute path for the bundled demo SQLite file.
10+
* The file is treated as a fixed app resource rather than a generated runtime artifact.
1111
*/
1212
export function resolveDemoSqlitePath(): string {
13-
const raw = process.env.PGLITE_DB_PATH;
14-
if (!raw) {
15-
throw new Error('[demo] PGLITE_DB_PATH is not set, cannot resolve demo.sqlite path');
16-
}
13+
return path.resolve(process.cwd(), DEMO_SQLITE_RELATIVE_PATH);
14+
}
1715

18-
const fsPath = raw.startsWith('file:') ? fileURLToPath(raw) : decodeURIComponent(raw);
19-
const parentDir = path.dirname(path.resolve(fsPath));
20-
return path.join(parentDir, DEMO_SQLITE_FILENAME);
16+
export function isDemoSqliteConnectionPath(value: string | null | undefined): boolean {
17+
return value?.trim() === DEMO_SQLITE_CONNECTION_PATH;
18+
}
19+
20+
export function resolveStoredSqlitePath(value: string | null | undefined): string | undefined {
21+
const normalized = value?.trim();
22+
if (!normalized) return undefined;
23+
if (isDemoSqliteConnectionPath(normalized)) {
24+
return resolveDemoSqlitePath();
25+
}
26+
return normalized;
2127
}
2228

2329
/**
24-
* Get the demo.sqlite path from the environment variable set during bootstrap.
30+
* Get the fixed absolute path for demo.sqlite.
2531
*/
2632
export function getDemoSqlitePath(): string | undefined {
27-
return process.env.DEMO_SQLITE_PATH || undefined;
33+
return resolveDemoSqlitePath();
2834
}

apps/web/next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const nextConfig: NextConfig = {
99
output: 'standalone',
1010
serverExternalPackages: ['@electric-sql/pglite', 'pino', 'better-sqlite3', 'electron'],
1111
outputFileTracingIncludes: {
12-
'/*': ['./registry/**/*'],
12+
'/*': ['./registry/**/*', './public/resources/demo.sqlite'],
1313
},
1414
logging: {
1515
fetches: {

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"pglite:studio": "drizzle-kit studio --config ./drizzle.pglite.config.ts",
1717
"drizzle:push": "drizzle-kit push",
1818
"drizzle:migrate": "tsx ./scripts/migrate.ts",
19+
"demo:backfill": "tsx ./scripts/backfill-demo-connections.ts",
1920
"postgres:check": "tsx ./scripts/check-postgres-connection.ts",
2021
"postgres:repair-migrations": "tsx ./scripts/repair-drizzle-migrations.ts",
2122
"postgres:migrate:generate": "drizzle-kit generate",

0 commit comments

Comments
 (0)