@rip-lang/db 0.10.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -259
- package/bin/rip-db +5 -10
- package/db.rip +217 -237
- package/lib/duckdb-binary.rip +34 -13
- package/lib/duckdb.mjs +694 -304
- package/package.json +10 -7
- package/PROTOCOL.md +0 -258
- package/db.html +0 -122
- package/lib/darwin-arm64/duckdb.node +0 -0
package/db.rip
CHANGED
|
@@ -6,45 +6,45 @@
|
|
|
6
6
|
# Fully compatible with the official DuckDB UI!
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
|
-
# rip db.rip <database.duckdb> [--port
|
|
10
|
-
# rip db.rip :memory: --port
|
|
9
|
+
# rip db.rip <database.duckdb> [--port 4213]
|
|
10
|
+
# rip db.rip :memory: --port 4213
|
|
11
11
|
#
|
|
12
|
-
# Then open http://localhost:
|
|
12
|
+
# Then open http://localhost:4213/ for the official DuckDB UI!
|
|
13
13
|
# (UI assets are proxied from ui.duckdb.org)
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# POST /ddb/run - DuckDB UI binary protocol
|
|
20
|
-
# GET /health - Health check
|
|
21
|
-
# GET /status - Database info
|
|
22
|
-
# GET /tables - List tables
|
|
23
|
-
# GET /schema/:table - Table schema
|
|
15
|
+
# Architecture:
|
|
16
|
+
# - Pure Bun FFI to DuckDB library (no npm package, no Zig)
|
|
17
|
+
# - Binary serialization in Rip for DuckDB UI protocol
|
|
18
|
+
# - Simple, clean, fast
|
|
24
19
|
#
|
|
25
20
|
# ==============================================================================
|
|
26
21
|
|
|
27
22
|
import { get, post, read, start, use } from '@rip-lang/api'
|
|
28
23
|
import { cors } from '@rip-lang/api/middleware'
|
|
29
|
-
import { open } from './lib/duckdb.mjs'
|
|
30
24
|
import { version as VERSION } from './package.json'
|
|
31
|
-
|
|
25
|
+
|
|
26
|
+
# Database access via pure Bun FFI
|
|
27
|
+
duckdb = await import('./lib/duckdb.mjs')
|
|
28
|
+
open = duckdb.open
|
|
29
|
+
duckdbVersion = duckdb.version
|
|
30
|
+
|
|
31
|
+
# Binary serialization for DuckDB UI protocol
|
|
32
|
+
{
|
|
32
33
|
serializeSuccessResult
|
|
33
34
|
serializeErrorResult
|
|
34
35
|
serializeEmptyResult
|
|
35
36
|
serializeTokenizeResult
|
|
36
37
|
tokenizeSQL
|
|
37
|
-
|
|
38
|
-
} from './lib/duckdb-binary.rip'
|
|
38
|
+
} = await import('./lib/duckdb-binary.rip')
|
|
39
39
|
|
|
40
|
-
# Enable CORS for
|
|
40
|
+
# Enable CORS for DuckDB UI and other clients
|
|
41
41
|
use cors preflight: true
|
|
42
42
|
|
|
43
43
|
# Log all requests
|
|
44
44
|
use (c, next) ->
|
|
45
|
-
|
|
45
|
+
t0 = Date.now()
|
|
46
46
|
result = next!
|
|
47
|
-
ms = Date.now() -
|
|
47
|
+
ms = Date.now() - t0
|
|
48
48
|
console.log "#{c.req.method} #{c.req.path} #{ms}ms"
|
|
49
49
|
result
|
|
50
50
|
|
|
@@ -58,7 +58,7 @@ args = process.argv.slice(2)
|
|
|
58
58
|
# Handle --help and --version
|
|
59
59
|
if '--help' in args or '-h' in args
|
|
60
60
|
console.log """
|
|
61
|
-
rip-db v#{VERSION} — DuckDB Server (Bun
|
|
61
|
+
rip-db v#{VERSION} — DuckDB Server (Pure Bun FFI)
|
|
62
62
|
|
|
63
63
|
Usage: rip db.rip [database] [options]
|
|
64
64
|
|
|
@@ -70,24 +70,17 @@ if '--help' in args or '-h' in args
|
|
|
70
70
|
--help Show this help
|
|
71
71
|
--version Show version
|
|
72
72
|
|
|
73
|
-
Environment variables (for rip-server):
|
|
74
|
-
DB_PATH Path to DuckDB file
|
|
75
|
-
DB_PORT Port to listen on
|
|
76
|
-
|
|
77
73
|
Endpoints:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
GET /
|
|
82
|
-
GET /
|
|
74
|
+
GET / Official DuckDB UI
|
|
75
|
+
POST /sql Execute SQL (JSON API)
|
|
76
|
+
POST /ddb/run DuckDB UI binary protocol
|
|
77
|
+
GET /health Health check
|
|
78
|
+
GET /tables List all tables
|
|
83
79
|
|
|
84
80
|
Examples:
|
|
85
81
|
rip db.rip # In-memory database on port 4213
|
|
86
82
|
rip db.rip mydb.duckdb # File-based database
|
|
87
83
|
rip db.rip :memory: --port=8080
|
|
88
|
-
|
|
89
|
-
# With rip-server (hot-reloading):
|
|
90
|
-
DB_PATH=mydb.duckdb rip-server http db.rip
|
|
91
84
|
"""
|
|
92
85
|
process.exit(0)
|
|
93
86
|
|
|
@@ -95,288 +88,277 @@ if '--version' in args or '-v' in args
|
|
|
95
88
|
console.log "rip-db v#{VERSION}"
|
|
96
89
|
process.exit(0)
|
|
97
90
|
|
|
98
|
-
#
|
|
91
|
+
# Database and port configuration
|
|
99
92
|
path = process.env.DB_PATH or args.find((a) -> not a.startsWith('-')) or ':memory:'
|
|
100
|
-
port = parseInt(process.env.DB_PORT or (args.find((a) -> a.startsWith('--port=')))?.split('=')[1]) or 4213
|
|
101
93
|
|
|
102
|
-
#
|
|
94
|
+
# Support both --port=N and --port N
|
|
95
|
+
portArg = do ->
|
|
96
|
+
for a, i in args
|
|
97
|
+
return a.split('=')[1] if a.startsWith('--port=')
|
|
98
|
+
return args[i + 1] if a is '--port' and args[i + 1]
|
|
99
|
+
null
|
|
100
|
+
port = parseInt(process.env.DB_PORT or portArg) or 4213
|
|
101
|
+
|
|
102
|
+
# Open database and create persistent connection
|
|
103
103
|
db = open(path)
|
|
104
|
-
|
|
104
|
+
conn = db.connect()
|
|
105
|
+
console.log "rip-db: database=#{path} (bun-ffi)"
|
|
106
|
+
console.log "rip-db: DuckDB version=#{duckdbVersion()}"
|
|
105
107
|
|
|
106
108
|
# ==============================================================================
|
|
107
109
|
# Helpers
|
|
108
110
|
# ==============================================================================
|
|
109
111
|
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
columns = Object.keys(first)
|
|
116
|
-
types = columns.map (col) ->
|
|
117
|
-
switch typeof val = first[col]
|
|
118
|
-
when 'number' then Number.isInteger(val) ? 'INTEGER' : 'DOUBLE'
|
|
119
|
-
when 'string' then 'VARCHAR'
|
|
120
|
-
when 'boolean' then 'BOOLEAN'
|
|
121
|
-
when 'bigint' then 'BIGINT'
|
|
122
|
-
else val is null ? 'NULL' : 'UNKNOWN'
|
|
123
|
-
{ columns, types }
|
|
124
|
-
|
|
125
|
-
# Convert row objects to arrays for compact format
|
|
126
|
-
rowsToArrays = (rows, columns) ->
|
|
127
|
-
rows.map (row) -> columns.map (col) -> row[col]
|
|
128
|
-
|
|
129
|
-
# Check if SQL is a SELECT-type query
|
|
112
|
+
# Helper for binary responses
|
|
113
|
+
binaryResponse = (data) ->
|
|
114
|
+
new Response data, headers: { 'Content-Type': 'application/octet-stream' }
|
|
115
|
+
|
|
116
|
+
# Check if SQL is a query that returns results
|
|
130
117
|
isSelectQuery = (sql) ->
|
|
131
118
|
upper = sql.trim().toUpperCase()
|
|
132
119
|
upper.startsWith('SELECT') or
|
|
133
120
|
upper.startsWith('WITH') or
|
|
134
121
|
upper.startsWith('SHOW') or
|
|
135
122
|
upper.startsWith('DESCRIBE') or
|
|
123
|
+
upper.startsWith('EXPLAIN') or
|
|
136
124
|
upper.startsWith('PRAGMA')
|
|
137
125
|
|
|
138
|
-
#
|
|
126
|
+
# Log SQL query (truncated if long)
|
|
127
|
+
logSQL = (sql) ->
|
|
128
|
+
preview = sql.replace(/\s+/g, ' ').trim()
|
|
129
|
+
preview = "#{preview.slice(0, 100)}..." if preview.length > 100
|
|
130
|
+
console.log "SQL: #{preview}"
|
|
131
|
+
|
|
132
|
+
# Execute SQL and return result (uses persistent connection)
|
|
139
133
|
executeSQL = (sql, params = []) ->
|
|
134
|
+
logSQL sql
|
|
140
135
|
startTime = Date.now()
|
|
141
|
-
conn = db.connect()
|
|
142
136
|
|
|
143
137
|
try
|
|
144
|
-
rows =
|
|
145
|
-
|
|
146
|
-
stmt.query(...params)
|
|
147
|
-
else
|
|
148
|
-
conn.query(sql)
|
|
138
|
+
rows = await conn.query(sql, params)
|
|
139
|
+
columns = rows.columns or []
|
|
149
140
|
|
|
150
141
|
if isSelectQuery(sql)
|
|
151
|
-
{ columns, types } = getColumnInfo(rows)
|
|
152
|
-
data = rowsToArrays(rows, columns)
|
|
153
142
|
{
|
|
154
|
-
meta: columns.map((
|
|
155
|
-
data:
|
|
156
|
-
rows:
|
|
143
|
+
meta: columns.map((col) -> { name: col.name, type: col.typeName })
|
|
144
|
+
data: rows.map((row) -> columns.map((col) -> row[col.name]))
|
|
145
|
+
rows: rows.length
|
|
157
146
|
time: (Date.now() - startTime) / 1000
|
|
158
147
|
}
|
|
159
148
|
else
|
|
160
149
|
{ meta: [], data: [], rows: 0, time: (Date.now() - startTime) / 1000 }
|
|
161
150
|
catch err
|
|
162
151
|
{ error: err.message }
|
|
163
|
-
finally
|
|
164
|
-
conn.close()
|
|
165
152
|
|
|
166
153
|
# ==============================================================================
|
|
167
|
-
# Endpoints
|
|
154
|
+
# JSON Endpoints (for custom apps)
|
|
168
155
|
# ==============================================================================
|
|
169
156
|
|
|
170
|
-
# POST / —
|
|
157
|
+
# POST /sql — Execute SQL with optional parameters
|
|
158
|
+
post '/sql' ->
|
|
159
|
+
contentType = @req.header('content-type') or ''
|
|
160
|
+
if contentType.includes('application/json')
|
|
161
|
+
body = read()
|
|
162
|
+
sql = body?.sql or body
|
|
163
|
+
params = body?.params or []
|
|
164
|
+
else
|
|
165
|
+
sql = read 'body', 'string'
|
|
166
|
+
params = []
|
|
167
|
+
return { error: 'Empty query' } unless sql
|
|
168
|
+
executeSQL sql, params
|
|
169
|
+
|
|
170
|
+
# POST / — Raw SQL in body (duck-ui compatible)
|
|
171
171
|
post '/' ->
|
|
172
172
|
sql = read 'body', 'string'
|
|
173
|
-
|
|
174
|
-
return { error: 'Empty query' }
|
|
173
|
+
return { error: 'Empty query' } unless sql
|
|
175
174
|
executeSQL sql
|
|
176
175
|
|
|
177
|
-
#
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
if not sql
|
|
181
|
-
return { error: 'Missing required field: sql' }
|
|
182
|
-
executeSQL sql, (params or [])
|
|
183
|
-
|
|
184
|
-
# GET /health — Simple health check (no DB query)
|
|
185
|
-
get '/health', ->
|
|
186
|
-
{ ok: true }
|
|
187
|
-
|
|
188
|
-
# GET /status — Database info and table list
|
|
189
|
-
get '/status', ->
|
|
190
|
-
conn = db.connect()
|
|
191
|
-
try
|
|
192
|
-
tables = conn.query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'")
|
|
193
|
-
{
|
|
194
|
-
ok: true
|
|
195
|
-
database: path
|
|
196
|
-
tables: tables.map((t) -> t.table_name)
|
|
197
|
-
time: new Date().toISOString()
|
|
198
|
-
}
|
|
199
|
-
catch err
|
|
200
|
-
{ ok: false, error: err.message }
|
|
201
|
-
finally
|
|
202
|
-
conn.close()
|
|
176
|
+
# GET /health — Health check
|
|
177
|
+
get '/health' ->
|
|
178
|
+
{ status: 'ok', version: VERSION }
|
|
203
179
|
|
|
204
180
|
# GET /tables — List all tables
|
|
205
|
-
get '/tables'
|
|
206
|
-
conn =
|
|
207
|
-
|
|
208
|
-
tables = conn.query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name")
|
|
209
|
-
{ ok: true, tables: tables.map((t) -> t.table_name) }
|
|
210
|
-
catch err
|
|
211
|
-
{ ok: false, error: err.message }
|
|
212
|
-
finally
|
|
213
|
-
conn.close()
|
|
181
|
+
get '/tables' ->
|
|
182
|
+
rows = await conn.query "SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'"
|
|
183
|
+
{ tables: rows.map((r) -> r.table_name) }
|
|
214
184
|
|
|
215
185
|
# GET /schema/:table — Get table schema
|
|
216
|
-
get '/schema/:table'
|
|
217
|
-
table =
|
|
218
|
-
conn = db.connect()
|
|
186
|
+
get '/schema/:table' ->
|
|
187
|
+
table = @req.params.table.replace(/[^a-zA-Z0-9_]/g, '') # sanitize
|
|
219
188
|
try
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if columns.length is 0
|
|
224
|
-
{ ok: false, error: "Table '#{table}' not found" }
|
|
225
|
-
else
|
|
226
|
-
{ ok: true, table: table, columns: columns }
|
|
189
|
+
rows = await conn.query "DESCRIBE \"#{table}\""
|
|
190
|
+
{ schema: rows }
|
|
227
191
|
catch err
|
|
228
|
-
{
|
|
229
|
-
finally
|
|
230
|
-
conn.close()
|
|
231
|
-
|
|
232
|
-
# GET /ui — Built-in SQL console
|
|
233
|
-
get '/ui', -> new Response Bun.file(import.meta.dir + '/db.html')
|
|
192
|
+
{ error: err.message }
|
|
234
193
|
|
|
235
194
|
# ==============================================================================
|
|
236
|
-
# DuckDB UI
|
|
237
|
-
# ==============================================================================
|
|
238
|
-
#
|
|
239
|
-
# These endpoints implement the binary protocol used by DuckDB's official UI.
|
|
240
|
-
# This allows you to use the beautiful DuckDB UI with rip-db!
|
|
241
|
-
#
|
|
242
|
-
# The official UI expects these endpoints:
|
|
243
|
-
# POST /ddb/run - Execute SQL (binary response)
|
|
244
|
-
# POST /ddb/interrupt - Cancel running query
|
|
245
|
-
# POST /ddb/tokenize - Tokenize SQL for syntax highlighting
|
|
246
|
-
# GET /info - Server version info
|
|
247
|
-
#
|
|
248
|
-
# To use the DuckDB UI:
|
|
249
|
-
# 1. Start rip-db: rip-db mydb.duckdb --port 4000
|
|
250
|
-
# 2. Open: http://localhost:4000 (UI proxied from ui.duckdb.org)
|
|
251
|
-
#
|
|
195
|
+
# DuckDB UI Binary Protocol
|
|
252
196
|
# ==============================================================================
|
|
253
197
|
|
|
198
|
+
# Remote UI URL for proxying assets
|
|
199
|
+
UI_REMOTE_URL = 'https://ui.duckdb.org'
|
|
200
|
+
|
|
201
|
+
# Helper to extract parameters from DuckDB UI headers (base64 encoded)
|
|
202
|
+
# The official C++ extension passes all params as strings (VARCHAR).
|
|
203
|
+
# DuckDB handles implicit casting to target types.
|
|
204
|
+
getUIParams = (req) ->
|
|
205
|
+
count = parseInt(req.header('x-duckdb-ui-parameter-count') or '0')
|
|
206
|
+
return [] if count is 0
|
|
207
|
+
|
|
208
|
+
params = []
|
|
209
|
+
for i in [0...count]
|
|
210
|
+
encoded = req.header("x-duckdb-ui-parameter-value-#{i}")
|
|
211
|
+
if encoded
|
|
212
|
+
try
|
|
213
|
+
decoded = atob(encoded)
|
|
214
|
+
# The UI sends literal "null" for NULL values
|
|
215
|
+
if decoded is 'null'
|
|
216
|
+
params.push null
|
|
217
|
+
else
|
|
218
|
+
params.push decoded
|
|
219
|
+
catch
|
|
220
|
+
params.push ''
|
|
221
|
+
else
|
|
222
|
+
params.push null
|
|
223
|
+
params
|
|
224
|
+
|
|
254
225
|
# POST /ddb/run — Execute SQL and return binary result (DuckDB UI protocol)
|
|
255
226
|
post '/ddb/run' ->
|
|
256
227
|
try
|
|
257
228
|
sql = read 'body', 'string'
|
|
258
|
-
|
|
259
|
-
return binaryResponse serializeErrorResult 'Empty query'
|
|
229
|
+
return binaryResponse serializeErrorResult 'Empty query' unless sql
|
|
260
230
|
|
|
261
|
-
|
|
231
|
+
logSQL sql
|
|
262
232
|
rowLimit = parseInt(@req.header('x-duckdb-ui-result-row-limit') or '10000')
|
|
233
|
+
params = getUIParams(@req)
|
|
263
234
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
try
|
|
267
|
-
rows = conn.query sql
|
|
235
|
+
rows = await conn.query sql, params
|
|
236
|
+
columns = rows.columns or []
|
|
268
237
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if rows?.length > 0
|
|
272
|
-
first = rows[0]
|
|
273
|
-
for key of first
|
|
274
|
-
columns.push { name: key, type: inferType first[key] }
|
|
238
|
+
# Build column info for serialization
|
|
239
|
+
colInfo = columns.map (col) -> { name: col.name, type: col.typeName }
|
|
275
240
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
241
|
+
# Convert rows to arrays (ensure undefined becomes null for serialization)
|
|
242
|
+
limitedRows = rows.slice(0, rowLimit)
|
|
243
|
+
arrayRows = limitedRows.map (row) ->
|
|
244
|
+
columns.map (col) ->
|
|
245
|
+
val = row[col.name]
|
|
246
|
+
if val is undefined then null else val
|
|
280
247
|
|
|
281
|
-
|
|
282
|
-
finally
|
|
283
|
-
conn.close()
|
|
248
|
+
binaryResponse serializeSuccessResult colInfo, arrayRows
|
|
284
249
|
|
|
285
250
|
catch err
|
|
286
|
-
|
|
287
|
-
|
|
251
|
+
msg = err?.message or 'Unknown error'
|
|
252
|
+
# Handle _duckdb_ui catalog queries - return empty success instead of error
|
|
253
|
+
if msg.includes('_duckdb_ui')
|
|
254
|
+
return binaryResponse serializeSuccessResult [], []
|
|
255
|
+
console.error "POST /ddb/run error:", msg
|
|
256
|
+
binaryResponse serializeErrorResult msg
|
|
288
257
|
|
|
289
258
|
# POST /ddb/interrupt — Cancel running query
|
|
290
|
-
post '/ddb/interrupt'
|
|
291
|
-
# In a real implementation, this would cancel the running query
|
|
292
|
-
# For now, just return empty result
|
|
259
|
+
post '/ddb/interrupt' ->
|
|
293
260
|
binaryResponse serializeEmptyResult()
|
|
294
261
|
|
|
295
262
|
# POST /ddb/tokenize — Tokenize SQL for syntax highlighting
|
|
296
263
|
post '/ddb/tokenize' ->
|
|
297
264
|
sql = read 'body', 'string'
|
|
298
|
-
tokens = tokenizeSQL
|
|
299
|
-
binaryResponse serializeTokenizeResult
|
|
265
|
+
tokens = tokenizeSQL(sql or '')
|
|
266
|
+
binaryResponse serializeTokenizeResult(tokens)
|
|
267
|
+
|
|
268
|
+
# ==============================================================================
|
|
269
|
+
# DuckDB UI Compatibility Endpoints
|
|
270
|
+
# (Responses match the official DuckDB UI extension exactly)
|
|
271
|
+
# ==============================================================================
|
|
300
272
|
|
|
301
|
-
#
|
|
273
|
+
# UI extension version — must match what /config advertises for our DuckDB version
|
|
274
|
+
UI_EXT_VERSION = '150-5582dfaffc'
|
|
275
|
+
|
|
276
|
+
# Detect platform for DuckDB headers
|
|
277
|
+
PLATFORM = switch "#{process.platform}_#{process.arch}"
|
|
278
|
+
when 'darwin_arm64' then 'osx_arm64'
|
|
279
|
+
when 'darwin_x64' then 'osx_amd64'
|
|
280
|
+
when 'linux_arm64' then 'linux_arm64'
|
|
281
|
+
when 'linux_x64' then 'linux_amd64'
|
|
282
|
+
else "#{process.platform}_#{process.arch}"
|
|
283
|
+
|
|
284
|
+
# GET /version — Tell UI we're running in host/HTTP mode (not WASM)
|
|
302
285
|
get '/version' ->
|
|
303
|
-
{ origin: '
|
|
286
|
+
{ origin: 'host', version: "main@e6517d259ec5f27ab713e5755a29d775a7750dc5" }
|
|
304
287
|
|
|
305
|
-
# GET /localToken — Return
|
|
288
|
+
# GET /localToken — Return 401 to skip MotherDuck auth (matches official)
|
|
306
289
|
get '/localToken' ->
|
|
307
|
-
new Response
|
|
290
|
+
new Response null, { status: 401 }
|
|
308
291
|
|
|
309
|
-
# GET /info — Server version info (
|
|
310
|
-
# The UI checks X-DuckDB-UI-Extension-Version to decide HTTP vs WASM mode
|
|
311
|
-
# Version format must match desiredDuckDBUIExtensionVersions in /config
|
|
292
|
+
# GET /info — Server version info (headers only, empty body — matches official)
|
|
312
293
|
get '/info' ->
|
|
313
294
|
@body '', 200,
|
|
314
295
|
'Access-Control-Allow-Origin': '*'
|
|
315
|
-
'
|
|
316
|
-
'X-DuckDB-
|
|
317
|
-
'X-DuckDB-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
296
|
+
'Content-Type': 'text/plain'
|
|
297
|
+
'X-DuckDB-Version': duckdbVersion()
|
|
298
|
+
'X-DuckDB-Platform': PLATFORM
|
|
299
|
+
'X-DuckDB-UI-Extension-Version': UI_EXT_VERSION
|
|
300
|
+
|
|
301
|
+
# GET /config — Proxy from ui.duckdb.org with DuckDB version headers
|
|
302
|
+
# The UI checks these HEADERS (not just /info) to decide HTTP vs WASM mode
|
|
303
|
+
get '/config' ->
|
|
304
|
+
try
|
|
305
|
+
response = fetch! "#{UI_REMOTE_URL}/config",
|
|
306
|
+
headers: { 'User-Agent': "rip-db/#{VERSION}" }
|
|
307
|
+
body = response.text!
|
|
308
|
+
new Response body,
|
|
309
|
+
status: response.status
|
|
310
|
+
headers:
|
|
311
|
+
'Content-Type': 'application/json'
|
|
312
|
+
'X-DuckDB-Version': duckdbVersion()
|
|
313
|
+
'X-DuckDB-Platform': PLATFORM
|
|
314
|
+
'X-DuckDB-UI-Extension-Version': UI_EXT_VERSION
|
|
315
|
+
catch
|
|
316
|
+
new Response JSON.stringify({
|
|
317
|
+
globalApiUrl: "http://localhost:#{port}"
|
|
318
|
+
apiUrl: "http://localhost:#{port}"
|
|
319
|
+
momApiUrl: "http://localhost:#{port}/mom"
|
|
320
|
+
logging: { enabled: false }
|
|
321
|
+
datadog: { applicationId: '', clientToken: '', site: '', env: 'local', allowedTracingOrigins: [] }
|
|
322
|
+
defaultFeatureValues: {}
|
|
323
|
+
desiredDuckDBUIExtensionVersions: { "#{duckdbVersion()}": UI_EXT_VERSION }
|
|
324
|
+
}),
|
|
325
|
+
headers:
|
|
326
|
+
'Content-Type': 'application/json'
|
|
327
|
+
'X-DuckDB-Version': duckdbVersion()
|
|
328
|
+
'X-DuckDB-Platform': PLATFORM
|
|
329
|
+
'X-DuckDB-UI-Extension-Version': UI_EXT_VERSION
|
|
337
330
|
|
|
338
|
-
# GET /localEvents —
|
|
331
|
+
# GET /localEvents — SSE stream with keepalive (matches official extension)
|
|
332
|
+
# The official blocks 5s per iteration, sends ":\r\r" keepalive if no events.
|
|
339
333
|
get '/localEvents' ->
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
intervalId = null
|
|
343
|
-
|
|
344
|
-
stream = new ReadableStream
|
|
334
|
+
keepalive = null
|
|
335
|
+
new Response new ReadableStream({
|
|
345
336
|
start: (controller) ->
|
|
346
|
-
# Send
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
# Keep connection alive with periodic heartbeats
|
|
350
|
-
intervalId = setInterval ->
|
|
337
|
+
# Send keepalive every 5 seconds (SSE comment, matches official ":\r\r")
|
|
338
|
+
keepalive = setInterval ->
|
|
351
339
|
try
|
|
352
|
-
controller.enqueue
|
|
340
|
+
controller.enqueue ":\r\r"
|
|
353
341
|
catch
|
|
354
|
-
clearInterval
|
|
355
|
-
,
|
|
356
|
-
|
|
342
|
+
clearInterval keepalive
|
|
343
|
+
, 5000
|
|
357
344
|
cancel: ->
|
|
358
|
-
clearInterval
|
|
359
|
-
|
|
360
|
-
new Response stream,
|
|
345
|
+
clearInterval keepalive if keepalive
|
|
346
|
+
}),
|
|
361
347
|
headers:
|
|
362
348
|
'Content-Type': 'text/event-stream'
|
|
363
|
-
|
|
364
|
-
|
|
349
|
+
|
|
350
|
+
# GET /mom/* — MotherDuck API stubs (prevent OAuth, return 401)
|
|
351
|
+
get '/mom/*' ->
|
|
352
|
+
new Response null, { status: 401 }
|
|
365
353
|
|
|
366
354
|
# GET /* — Proxy UI assets from ui.duckdb.org (catch-all, must be last)
|
|
367
355
|
get '/*' ->
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
# Skip if this is one of our API endpoints (shouldn't reach here, but safety)
|
|
371
|
-
return { error: 'Not found' } if path in ['/health', '/status', '/tables', '/info', '/ui']
|
|
356
|
+
reqPath = @req.path
|
|
372
357
|
|
|
373
358
|
try
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
headers:
|
|
377
|
-
'User-Agent': "rip-db/#{VERSION}"
|
|
359
|
+
response = fetch! "#{UI_REMOTE_URL}#{reqPath}",
|
|
360
|
+
headers: { 'User-Agent': "rip-db/#{VERSION}" }
|
|
378
361
|
|
|
379
|
-
# Get response body and content type
|
|
380
362
|
body = response.arrayBuffer!
|
|
381
363
|
contentType = response.headers.get('Content-Type') or 'application/octet-stream'
|
|
382
364
|
|
|
@@ -384,18 +366,17 @@ get '/*' ->
|
|
|
384
366
|
headers =
|
|
385
367
|
'Content-Type': contentType
|
|
386
368
|
'Cache-Control': response.headers.get('Cache-Control') or 'public, max-age=3600'
|
|
369
|
+
# COOP/COEP required for SharedArrayBuffer (DuckDB WASM fallback)
|
|
370
|
+
'Cross-Origin-Opener-Policy': 'same-origin'
|
|
371
|
+
'Cross-Origin-Embedder-Policy': 'credentialless'
|
|
387
372
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
headers
|
|
391
|
-
headers['X-DuckDB-Platform'] = 'rip-db'
|
|
392
|
-
headers['X-DuckDB-UI-Extension-Version'] = '139-944c08a214'
|
|
393
|
-
|
|
394
|
-
new Response body, { status: response.status, headers }
|
|
373
|
+
new Response body,
|
|
374
|
+
status: response.status
|
|
375
|
+
headers: headers
|
|
395
376
|
|
|
396
377
|
catch err
|
|
397
|
-
console.error "Proxy error for #{
|
|
398
|
-
new Response "Failed to fetch UI asset: #{
|
|
378
|
+
console.error "Proxy error for #{reqPath}:", err?.message
|
|
379
|
+
new Response "Failed to fetch UI asset: #{reqPath}", { status: 502 }
|
|
399
380
|
|
|
400
381
|
# ==============================================================================
|
|
401
382
|
# Start Server
|
|
@@ -404,5 +385,4 @@ get '/*' ->
|
|
|
404
385
|
start port: port
|
|
405
386
|
|
|
406
387
|
console.log "rip-db: listening on http://localhost:#{port}"
|
|
407
|
-
console.log "rip-db:
|
|
408
|
-
console.log "rip-db: Built-in console at http://localhost:#{port}/ui"
|
|
388
|
+
console.log "rip-db: DuckDB UI available at http://localhost:#{port}/"
|