Skip to content

Commit c8b10c6

Browse files
authored
feat(db_studio): Add DB studio package (#392)
Adds a wrapper around Outerbase Studio for Celest databases.
1 parent 937686c commit c8b10c6

File tree

17 files changed

+1021
-0
lines changed

17 files changed

+1021
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: celest_db_studio
2+
on:
3+
pull_request:
4+
paths:
5+
- ".github/workflows/celest_db_studio.yaml"
6+
- "packages/celest_db_studio/**"
7+
8+
# Prevent duplicate runs due to Graphite
9+
# https://graphite.dev/docs/troubleshooting#why-are-my-actions-running-twice
10+
concurrency:
11+
group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }}-${{ github.ref == 'refs/heads/main' && github.sha || ''}}
12+
cancel-in-progress: true
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
test:
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 10
21+
steps:
22+
- name: Git Checkout
23+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
24+
- name: Setup Flutter
25+
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # 2.19.0
26+
with:
27+
cache: true
28+
- name: Get Packages
29+
working-directory: packages/celest_db_studio
30+
run: dart pub upgrade
31+
- name: Setup Chromedriver
32+
uses: nanasess/setup-chromedriver@e93e57b843c0c92788f22483f1a31af8ee48db25 # 2.3.0
33+
- name: Test
34+
working-directory: packages/celest_db_studio
35+
run: dart test --fail-fast
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# https://dart.dev/guides/libraries/private-files
2+
# Created by `dart pub`
3+
.dart_tool/
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 1.0.0
2+
3+
- Initial version.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Celest Cloud DB Studio
2+
3+
A wrapper over [Outerbase Studio](https://github.com/outerbase/studio) for Celest Cloud databases.
4+
5+
This is used by Celest CLI to provide a local database studio when running `celest start` locally.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file configures the static analysis results for your project (errors,
2+
# warnings, and lints).
3+
#
4+
# This enables the 'recommended' set of lints from `package:lints`.
5+
# This set helps identify many issues that may lead to problems when running
6+
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
7+
# style and format.
8+
#
9+
# If you want a smaller set of lints you can change this to specify
10+
# 'package:lints/core.yaml'. These are just the most critical lints
11+
# (the recommended set includes the core lints).
12+
# The core lints are also what is used by pub.dev for scoring packages.
13+
14+
include: package:lints/recommended.yaml
15+
16+
# Uncomment the following section to specify additional rules.
17+
18+
# linter:
19+
# rules:
20+
# - camel_case_types
21+
22+
# analyzer:
23+
# exclude:
24+
# - path/to/excluded/files/**
25+
26+
# For more information about the core and recommended set of lints, see
27+
# https://dart.dev/go/core-lints
28+
29+
# For additional information about configuring this file, see
30+
# https://dart.dev/guides/language/analysis-options
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'dart:io';
2+
3+
import 'package:celest_db_studio/celest_db_studio.dart';
4+
import 'package:shelf/shelf.dart';
5+
import 'package:shelf/shelf_io.dart';
6+
7+
Future<void> main(List<String> args) async {
8+
final databaseUrl = Platform.environment['DATABASE_URL'];
9+
if (databaseUrl == null) {
10+
print('DATABASE_URL environment variable is not set.');
11+
exit(1);
12+
}
13+
final authToken = Platform.environment['DATABASE_AUTH_TOKEN'];
14+
15+
final dbStudio = await CelestDbStudio.create(
16+
databaseUri: Uri.parse(databaseUrl),
17+
authToken: authToken,
18+
);
19+
final handler = Pipeline()
20+
.addMiddleware(logRequests())
21+
.addHandler(dbStudio.call);
22+
23+
final port = int.parse(Platform.environment['PORT'] ?? '8080');
24+
final server = await serve(handler, InternetAddress.loopbackIPv4, port);
25+
print('Server listening on http://localhost:${server.port}');
26+
27+
await ProcessSignal.sigint.watch().first;
28+
29+
print('Stopping server...');
30+
await server.close(force: true);
31+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import 'dart:convert';
2+
3+
import 'package:celest_core/_internal.dart';
4+
import 'package:celest_db_studio/src/driver.dart';
5+
import 'package:celest_db_studio/src/template.dart';
6+
import 'package:shelf/shelf.dart';
7+
import 'package:shelf_router/shelf_router.dart';
8+
9+
export 'package:celest_db_studio/src/driver.dart';
10+
11+
/// {@template celest_db_studio.celest_db_studio}
12+
/// A simple server which serves an instance of Outerbase Studio as an embedded
13+
/// iframe and responds to query requests from the iframe.
14+
///
15+
/// The server connects to a database using the provided [databaseUri] and
16+
/// proxies requests from the iframe to the database.
17+
/// {@endtemplate}
18+
final class CelestDbStudio {
19+
/// {@macro celest_db_studio.celest_db_studio}
20+
static Future<CelestDbStudio> create({
21+
String pageTitle = defaultTitle,
22+
required Uri databaseUri,
23+
String? authToken,
24+
}) async {
25+
final Driver driver;
26+
switch (databaseUri) {
27+
case Uri(scheme: 'libsql' || 'https' || 'http'):
28+
driver = await HranaDriver.connect(databaseUri, jwtToken: authToken);
29+
case Uri(scheme: 'file', path: '/:memory:'):
30+
driver = NativeDriver.memory();
31+
case Uri(scheme: 'file'):
32+
driver = NativeDriver.file(databaseUri.toFilePath());
33+
default:
34+
throw ArgumentError.value(
35+
databaseUri.toString(),
36+
'databaseUri',
37+
'Unsupported database URI scheme: ${databaseUri.scheme}. '
38+
'Supported schemes are: libsql, https, http, file',
39+
);
40+
}
41+
42+
return CelestDbStudio.from(pageTitle: pageTitle, driver: driver);
43+
}
44+
45+
CelestDbStudio.from({this.pageTitle = defaultTitle, required Driver driver})
46+
: _driver = driver;
47+
48+
/// The default title of the page.
49+
static const String defaultTitle = 'DB Studio';
50+
51+
/// The title of the page.
52+
///
53+
/// If not provided, the [defaultTitle] will be used.
54+
final String pageTitle;
55+
56+
final Driver _driver;
57+
58+
/// The rendered HTML for the index page.
59+
late final String _indexHtml = indexHtml
60+
.replaceAll('{{ title }}', pageTitle)
61+
.replaceAll('{{ script }}', indexJs);
62+
63+
late final Router _router =
64+
Router()
65+
..get('/', _index)
66+
..get('/index.html', _index)
67+
..post('/query', _query);
68+
69+
late final Handler _handler = (const Pipeline()
70+
..addMiddleware(_corsMiddleware))
71+
.addHandler(_router.call);
72+
73+
static Handler _corsMiddleware(Handler innerHandler) {
74+
const corsHeaders = {
75+
'Access-Control-Allow-Origin': '*',
76+
'Access-Control-Allow-Methods': '*',
77+
'Access-Control-Allow-Headers': '*',
78+
'Access-Control-Allow-Credentials': 'true',
79+
};
80+
return (Request request) async {
81+
if (request.method == 'OPTIONS') {
82+
return Response.ok(null, headers: corsHeaders);
83+
}
84+
final response = await innerHandler(request);
85+
return response.change(headers: corsHeaders);
86+
};
87+
}
88+
89+
/// Handles the given [request] if possible.
90+
Future<Response> call(Request request) async {
91+
return _handler(request);
92+
}
93+
94+
/// Serves the studio HTML.
95+
Future<Response> _index(Request request) async {
96+
return Response.ok(_indexHtml, headers: {'Content-Type': 'text/html'});
97+
}
98+
99+
/// Responds to query requests from the Outerbase Studio iframe.
100+
Future<Response> _query(Request request) async {
101+
final json = await JsonUtf8.decodeStream(request.read());
102+
if (json
103+
case {
104+
'id': final int id,
105+
'type': final String type,
106+
'statements': [final String statement],
107+
} ||
108+
{
109+
'id': final int id,
110+
'type': final String type,
111+
'statement': final String statement,
112+
}) {
113+
try {
114+
final result = await _driver.execute(statement);
115+
return Response.ok(
116+
jsonEncode({
117+
'type': type,
118+
'id': id,
119+
'data': type == 'transaction' ? [result.toJson()] : result.toJson(),
120+
}),
121+
headers: {'Content-Type': 'application/json'},
122+
);
123+
} on Object catch (e) {
124+
return Response.internalServerError(body: e.toString());
125+
}
126+
} else {
127+
return Response.badRequest(body: 'Invalid request format');
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)