orez 0.2.27 → 0.2.30
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/dist/cf-do/worker.d.ts +3 -0
- package/dist/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +37 -15
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/admin/admin-data.test.ts +0 -348
- package/src/admin/http-proxy.ts +0 -252
- package/src/admin/log-store.ts +0 -192
- package/src/admin/server.ts +0 -471
- package/src/admin/ui.ts +0 -1322
- package/src/bench/proxy-throughput.bench.ts +0 -343
- package/src/bench/serial-mutations.bench.ts +0 -270
- package/src/browser.ts +0 -203
- package/src/cf-do/.wrangler/cache/cf.json +0 -1
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
- package/src/cf-do/ARCHITECTURE.md +0 -93
- package/src/cf-do/CHAT_E2E.md +0 -213
- package/src/cf-do/watermark.test.ts +0 -103
- package/src/cf-do/watermark.ts +0 -118
- package/src/cf-do/worker.ts +0 -1041
- package/src/cf-do/wrangler.toml +0 -11
- package/src/cf-pglite/README.md +0 -19
- package/src/change-tracking.ts +0 -25
- package/src/child-process.test.ts +0 -147
- package/src/child-process.ts +0 -90
- package/src/cli-entry.ts +0 -72
- package/src/cli.test.ts +0 -40
- package/src/cli.ts +0 -1214
- package/src/config.ts +0 -150
- package/src/do-sql-tracking.test.ts +0 -19
- package/src/do-sql-tracking.ts +0 -19
- package/src/index.ts +0 -1215
- package/src/integration/integration.test.ts +0 -517
- package/src/integration/native-binary.guard.test.ts +0 -13
- package/src/integration/native-startup.test.ts +0 -44
- package/src/integration/replication-latency.test.ts +0 -428
- package/src/integration/restore-live-stress.test.ts +0 -433
- package/src/integration/restore-reset.test.ts +0 -400
- package/src/integration/restore.test.ts +0 -274
- package/src/integration/test-permissions.ts +0 -147
- package/src/load-config.ts +0 -46
- package/src/log.ts +0 -96
- package/src/mutex.ts +0 -47
- package/src/pg-proxy-browser.singledb.test.ts +0 -233
- package/src/pg-proxy-browser.ts +0 -2022
- package/src/pg-proxy-do-backend.test.ts +0 -3890
- package/src/pg-proxy-do-backend.ts +0 -7191
- package/src/pg-proxy.ts +0 -1087
- package/src/pg-sqlite-compiler/README.md +0 -53
- package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
- package/src/pg-sqlite-compiler/index.ts +0 -73
- package/src/pg-sqlite-compiler/integration.test.ts +0 -136
- package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
- package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
- package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
- package/src/pg-sqlite-compiler/passes/index.ts +0 -49
- package/src/pg-sqlite-compiler/passes/types.ts +0 -156
- package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
- package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
- package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
- package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
- package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
- package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
- package/src/pg-sqlite-compiler/types.ts +0 -63
- package/src/pglite-ipc.test.ts +0 -116
- package/src/pglite-ipc.ts +0 -266
- package/src/pglite-manager.ts +0 -557
- package/src/pglite-web-proxy.test.ts +0 -57
- package/src/pglite-web-proxy.ts +0 -221
- package/src/pglite-web-worker.ts +0 -152
- package/src/pglite-worker-thread.ts +0 -253
- package/src/port.ts +0 -25
- package/src/process-title.ts +0 -9
- package/src/recovery.ts +0 -155
- package/src/replication/change-tracker.test.ts +0 -357
- package/src/replication/change-tracker.ts +0 -279
- package/src/replication/handler.test.ts +0 -511
- package/src/replication/handler.ts +0 -1190
- package/src/replication/pgoutput-encoder.test.ts +0 -697
- package/src/replication/pgoutput-encoder.ts +0 -373
- package/src/replication/tcp-replication.test.ts +0 -876
- package/src/replication/zero-compat.test.ts +0 -1150
- package/src/restore-stress.test.ts +0 -188
- package/src/s3-local.ts +0 -203
- package/src/shim/hooks.mjs +0 -120
- package/src/shim/register.mjs +0 -4
- package/src/sqlite-mode/apply-mode.ts +0 -224
- package/src/sqlite-mode/index.ts +0 -15
- package/src/sqlite-mode/native-binary.ts +0 -89
- package/src/sqlite-mode/package-resolve.ts +0 -17
- package/src/sqlite-mode/resolve-mode.ts +0 -80
- package/src/sqlite-mode/shim-template.ts +0 -159
- package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
- package/src/sqlite-mode/types.ts +0 -30
- package/src/vite-plugin.ts +0 -67
- package/src/wasm-sqlite.test.ts +0 -537
- package/src/worker/browser-admin.ts +0 -52
- package/src/worker/browser-build-config.test.ts +0 -71
- package/src/worker/browser-build-config.ts +0 -109
- package/src/worker/browser-embed-admin.test.ts +0 -75
- package/src/worker/browser-embed.ts +0 -345
- package/src/worker/cf-patches.ts +0 -384
- package/src/worker/embed-integration.test.ts +0 -321
- package/src/worker/index.ts +0 -138
- package/src/worker/shims/fastify.test.ts +0 -255
- package/src/worker/shims/fastify.ts +0 -306
- package/src/worker/shims/http-service.test.ts +0 -355
- package/src/worker/shims/http-service.ts +0 -293
- package/src/worker/shims/node-stub.ts +0 -290
- package/src/worker/shims/oxfmt.ts +0 -3
- package/src/worker/shims/postgres-browser.ts +0 -59
- package/src/worker/shims/postgres-socket.test.ts +0 -576
- package/src/worker/shims/postgres-socket.ts +0 -310
- package/src/worker/shims/postgres.test.ts +0 -364
- package/src/worker/shims/postgres.ts +0 -1454
- package/src/worker/shims/sqlite-browser.test.ts +0 -233
- package/src/worker/shims/sqlite-browser.ts +0 -175
- package/src/worker/shims/sqlite.test.ts +0 -786
- package/src/worker/shims/sqlite.ts +0 -978
- package/src/worker/shims/stream-browser.ts +0 -15
- package/src/worker/shims/ws-browser.test.ts +0 -205
- package/src/worker/shims/ws-browser.ts +0 -248
- package/src/worker/shims/ws.test.ts +0 -288
- package/src/worker/shims/ws.ts +0 -467
- package/src/worker/shims/zero-process-env.ts +0 -11
- package/src/worker/types.ts +0 -75
- package/src/worker/worker-integration.test.ts +0 -223
- package/src/worker/worker.test.ts +0 -136
- package/src/worker/zero-cache-embed-cf.ts +0 -463
- package/src/worker/zero-cache-embed.ts +0 -277
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* integration tests for the admin data explorer endpoints.
|
|
3
|
-
*
|
|
4
|
-
* spins up pglite instances + admin server directly (no zero-cache)
|
|
5
|
-
* and exercises the /api/db/* and /api/sqlite/* endpoints.
|
|
6
|
-
*/
|
|
7
|
-
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
|
-
import { resolve } from 'node:path'
|
|
9
|
-
|
|
10
|
-
import { PGlite } from '@electric-sql/pglite'
|
|
11
|
-
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
|
12
|
-
|
|
13
|
-
import { startAdminServer } from './server.js'
|
|
14
|
-
|
|
15
|
-
import type { ZeroLiteConfig } from '../config.js'
|
|
16
|
-
import type { LogStore } from './log-store.js'
|
|
17
|
-
import type { Server } from 'node:http'
|
|
18
|
-
|
|
19
|
-
const TEST_PORT = 16400 + Math.floor(Math.random() * 500)
|
|
20
|
-
const DATA_DIR = `.orez-admin-data-test-${Date.now()}`
|
|
21
|
-
|
|
22
|
-
function stubLogStore(): LogStore {
|
|
23
|
-
const entries: any[] = []
|
|
24
|
-
return {
|
|
25
|
-
push() {},
|
|
26
|
-
query() {
|
|
27
|
-
return { entries, cursor: 0 }
|
|
28
|
-
},
|
|
29
|
-
getAll() {
|
|
30
|
-
return entries
|
|
31
|
-
},
|
|
32
|
-
clear() {
|
|
33
|
-
entries.length = 0
|
|
34
|
-
},
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function stubConfig(): ZeroLiteConfig {
|
|
39
|
-
return {
|
|
40
|
-
dataDir: resolve(DATA_DIR),
|
|
41
|
-
pgPort: 0,
|
|
42
|
-
zeroPort: 0,
|
|
43
|
-
adminPort: TEST_PORT,
|
|
44
|
-
pgUser: 'test',
|
|
45
|
-
pgPassword: 'test',
|
|
46
|
-
migrationsDir: '',
|
|
47
|
-
seedFile: '',
|
|
48
|
-
skipZeroCache: true,
|
|
49
|
-
disableWasmSqlite: false,
|
|
50
|
-
forceWasmSqlite: false,
|
|
51
|
-
useWorkerThreads: false,
|
|
52
|
-
singleDb: false,
|
|
53
|
-
readReplicas: 0,
|
|
54
|
-
logLevel: 'info',
|
|
55
|
-
pgliteOptions: {},
|
|
56
|
-
checkpointIntervalMs: 0,
|
|
57
|
-
maxLogFileSize: 0,
|
|
58
|
-
disableDiskLogs: true,
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
describe('admin data explorer', { timeout: 60_000 }, () => {
|
|
63
|
-
let server: Server
|
|
64
|
-
let postgres: PGlite
|
|
65
|
-
let cvr: PGlite
|
|
66
|
-
let cdb: PGlite
|
|
67
|
-
const base = `http://127.0.0.1:${TEST_PORT}`
|
|
68
|
-
|
|
69
|
-
beforeAll(async () => {
|
|
70
|
-
mkdirSync(DATA_DIR, { recursive: true })
|
|
71
|
-
|
|
72
|
-
postgres = new PGlite()
|
|
73
|
-
cvr = new PGlite()
|
|
74
|
-
cdb = new PGlite()
|
|
75
|
-
await Promise.all([postgres.waitReady, cvr.waitReady, cdb.waitReady])
|
|
76
|
-
|
|
77
|
-
// create test tables
|
|
78
|
-
await postgres.exec(`
|
|
79
|
-
CREATE TABLE public.users (
|
|
80
|
-
id serial PRIMARY KEY,
|
|
81
|
-
name text NOT NULL,
|
|
82
|
-
email text,
|
|
83
|
-
active boolean DEFAULT true
|
|
84
|
-
);
|
|
85
|
-
INSERT INTO public.users (name, email) VALUES
|
|
86
|
-
('alice', 'alice@test.com'),
|
|
87
|
-
('bob', 'bob@test.com'),
|
|
88
|
-
('charlie', 'charlie@test.com');
|
|
89
|
-
`)
|
|
90
|
-
|
|
91
|
-
await postgres.exec(`
|
|
92
|
-
CREATE TABLE public.posts (
|
|
93
|
-
id serial PRIMARY KEY,
|
|
94
|
-
user_id int REFERENCES users(id),
|
|
95
|
-
title text NOT NULL,
|
|
96
|
-
body text
|
|
97
|
-
);
|
|
98
|
-
INSERT INTO public.posts (user_id, title, body) VALUES
|
|
99
|
-
(1, 'hello world', 'first post'),
|
|
100
|
-
(1, 'second post', 'more content'),
|
|
101
|
-
(2, 'bob writes', NULL);
|
|
102
|
-
`)
|
|
103
|
-
|
|
104
|
-
server = await startAdminServer({
|
|
105
|
-
port: TEST_PORT,
|
|
106
|
-
logStore: stubLogStore(),
|
|
107
|
-
config: stubConfig(),
|
|
108
|
-
zeroEnv: {},
|
|
109
|
-
startTime: Date.now(),
|
|
110
|
-
db: { postgres, cvr, cdb, postgresReplicas: [] } as any,
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
afterAll(async () => {
|
|
115
|
-
server?.close()
|
|
116
|
-
await Promise.all([postgres?.close(), cvr?.close(), cdb?.close()])
|
|
117
|
-
rmSync(DATA_DIR, { recursive: true, force: true })
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
// --- html ---
|
|
121
|
-
|
|
122
|
-
test('GET / serves html', async () => {
|
|
123
|
-
const res = await fetch(`${base}/`)
|
|
124
|
-
expect(res.status).toBe(200)
|
|
125
|
-
expect(res.headers.get('content-type')).toContain('text/html')
|
|
126
|
-
const html = await res.text()
|
|
127
|
-
expect(html).toContain('oreZ admin')
|
|
128
|
-
expect(html).toContain('data-db="sqlite"')
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('GET /data serves html', async () => {
|
|
132
|
-
const res = await fetch(`${base}/data`)
|
|
133
|
-
expect(res.status).toBe(200)
|
|
134
|
-
const html = await res.text()
|
|
135
|
-
expect(html).toContain('sql-editor')
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
// --- /api/db/tables ---
|
|
139
|
-
|
|
140
|
-
test('lists postgres tables', async () => {
|
|
141
|
-
const res = await fetch(`${base}/api/db/tables?db=postgres`)
|
|
142
|
-
expect(res.status).toBe(200)
|
|
143
|
-
const data = await res.json()
|
|
144
|
-
expect(data.tables).toBeDefined()
|
|
145
|
-
const names = data.tables.map((t: any) => t.table_name)
|
|
146
|
-
expect(names).toContain('users')
|
|
147
|
-
expect(names).toContain('posts')
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
test('lists cvr tables (empty)', async () => {
|
|
151
|
-
const res = await fetch(`${base}/api/db/tables?db=cvr`)
|
|
152
|
-
expect(res.status).toBe(200)
|
|
153
|
-
const data = await res.json()
|
|
154
|
-
expect(data.tables).toBeDefined()
|
|
155
|
-
expect(data.tables.length).toBe(0)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
test('rejects unknown db name', async () => {
|
|
159
|
-
const res = await fetch(`${base}/api/db/tables?db=nope`)
|
|
160
|
-
expect(res.status).toBe(400)
|
|
161
|
-
const data = await res.json()
|
|
162
|
-
expect(data.error).toContain('unknown db')
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// --- /api/db/table-data ---
|
|
166
|
-
|
|
167
|
-
test('browses table data', async () => {
|
|
168
|
-
const res = await fetch(`${base}/api/db/table-data?db=postgres&table=users`)
|
|
169
|
-
expect(res.status).toBe(200)
|
|
170
|
-
const data = await res.json()
|
|
171
|
-
expect(data.columns).toBeDefined()
|
|
172
|
-
expect(data.columns.length).toBeGreaterThanOrEqual(4)
|
|
173
|
-
expect(data.rows.length).toBe(3)
|
|
174
|
-
expect(data.total).toBe(3)
|
|
175
|
-
// check column metadata
|
|
176
|
-
const colNames = data.columns.map((c: any) => c.name)
|
|
177
|
-
expect(colNames).toContain('id')
|
|
178
|
-
expect(colNames).toContain('name')
|
|
179
|
-
expect(colNames).toContain('email')
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
test('table-data supports search', async () => {
|
|
183
|
-
const res = await fetch(
|
|
184
|
-
`${base}/api/db/table-data?db=postgres&table=users&search=alice`
|
|
185
|
-
)
|
|
186
|
-
const data = await res.json()
|
|
187
|
-
expect(data.rows.length).toBe(1)
|
|
188
|
-
expect(data.rows[0].name).toBe('alice')
|
|
189
|
-
expect(data.total).toBe(1)
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
test('table-data supports pagination', async () => {
|
|
193
|
-
const res = await fetch(
|
|
194
|
-
`${base}/api/db/table-data?db=postgres&table=users&limit=2&offset=0`
|
|
195
|
-
)
|
|
196
|
-
const data = await res.json()
|
|
197
|
-
expect(data.rows.length).toBe(2)
|
|
198
|
-
expect(data.total).toBe(3)
|
|
199
|
-
|
|
200
|
-
const page2 = await fetch(
|
|
201
|
-
`${base}/api/db/table-data?db=postgres&table=users&limit=2&offset=2`
|
|
202
|
-
)
|
|
203
|
-
const data2 = await page2.json()
|
|
204
|
-
expect(data2.rows.length).toBe(1)
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
test('table-data with schema-qualified name', async () => {
|
|
208
|
-
const res = await fetch(`${base}/api/db/table-data?db=postgres&table=public.posts`)
|
|
209
|
-
const data = await res.json()
|
|
210
|
-
expect(data.rows.length).toBe(3)
|
|
211
|
-
// check NULL values come through
|
|
212
|
-
const bobPost = data.rows.find((r: any) => r.title === 'bob writes')
|
|
213
|
-
expect(bobPost).toBeDefined()
|
|
214
|
-
expect(bobPost.body).toBeNull()
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
test('table-data missing table param', async () => {
|
|
218
|
-
const res = await fetch(`${base}/api/db/table-data?db=postgres`)
|
|
219
|
-
expect(res.status).toBe(400)
|
|
220
|
-
const data = await res.json()
|
|
221
|
-
expect(data.error).toContain('missing table')
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
// --- /api/db/query ---
|
|
225
|
-
|
|
226
|
-
test('runs arbitrary SQL', async () => {
|
|
227
|
-
const res = await fetch(`${base}/api/db/query`, {
|
|
228
|
-
method: 'POST',
|
|
229
|
-
headers: { 'Content-Type': 'application/json' },
|
|
230
|
-
body: JSON.stringify({
|
|
231
|
-
db: 'postgres',
|
|
232
|
-
sql: 'SELECT name, email FROM users ORDER BY name',
|
|
233
|
-
}),
|
|
234
|
-
})
|
|
235
|
-
expect(res.status).toBe(200)
|
|
236
|
-
const data = await res.json()
|
|
237
|
-
expect(data.fields).toEqual(['name', 'email'])
|
|
238
|
-
expect(data.rowCount).toBe(3)
|
|
239
|
-
expect(data.rows[0].name).toBe('alice')
|
|
240
|
-
expect(data.durationMs).toBeGreaterThanOrEqual(0)
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
test('returns error for bad SQL', async () => {
|
|
244
|
-
const res = await fetch(`${base}/api/db/query`, {
|
|
245
|
-
method: 'POST',
|
|
246
|
-
headers: { 'Content-Type': 'application/json' },
|
|
247
|
-
body: JSON.stringify({ db: 'postgres', sql: 'SELECT * FROM nonexistent' }),
|
|
248
|
-
})
|
|
249
|
-
expect(res.status).toBe(400)
|
|
250
|
-
const data = await res.json()
|
|
251
|
-
expect(data.error).toBeTruthy()
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
test('query with joins', async () => {
|
|
255
|
-
const res = await fetch(`${base}/api/db/query`, {
|
|
256
|
-
method: 'POST',
|
|
257
|
-
headers: { 'Content-Type': 'application/json' },
|
|
258
|
-
body: JSON.stringify({
|
|
259
|
-
db: 'postgres',
|
|
260
|
-
sql: `SELECT u.name, p.title FROM users u JOIN posts p ON p.user_id = u.id ORDER BY p.id`,
|
|
261
|
-
}),
|
|
262
|
-
})
|
|
263
|
-
const data = await res.json()
|
|
264
|
-
expect(data.rowCount).toBe(3)
|
|
265
|
-
expect(data.fields).toEqual(['name', 'title'])
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
test('query missing sql', async () => {
|
|
269
|
-
const res = await fetch(`${base}/api/db/query`, {
|
|
270
|
-
method: 'POST',
|
|
271
|
-
headers: { 'Content-Type': 'application/json' },
|
|
272
|
-
body: JSON.stringify({ db: 'postgres' }),
|
|
273
|
-
})
|
|
274
|
-
expect(res.status).toBe(400)
|
|
275
|
-
const data = await res.json()
|
|
276
|
-
expect(data.error).toContain('missing sql')
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
// --- sqlite ---
|
|
280
|
-
|
|
281
|
-
test('sqlite tables returns 404 when no replica', async () => {
|
|
282
|
-
const res = await fetch(`${base}/api/sqlite/tables`)
|
|
283
|
-
expect(res.status).toBe(404)
|
|
284
|
-
const data = await res.json()
|
|
285
|
-
expect(data.error).toContain('not found')
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
test('sqlite endpoints work when replica file exists', async () => {
|
|
289
|
-
// create a fake zero-replica.db using bedrock-sqlite
|
|
290
|
-
// @ts-expect-error - CJS module
|
|
291
|
-
const bedrock: any = await import('bedrock-sqlite')
|
|
292
|
-
const Ctor = bedrock.Database || bedrock.default?.Database || bedrock.default
|
|
293
|
-
const replicaPath = resolve(DATA_DIR, 'zero-replica.db')
|
|
294
|
-
const setupDb = new Ctor(replicaPath)
|
|
295
|
-
setupDb.exec(`
|
|
296
|
-
CREATE TABLE widgets (
|
|
297
|
-
id INTEGER PRIMARY KEY,
|
|
298
|
-
label TEXT NOT NULL,
|
|
299
|
-
count INTEGER
|
|
300
|
-
);
|
|
301
|
-
INSERT INTO widgets (label, count) VALUES
|
|
302
|
-
('alpha', 1),
|
|
303
|
-
('beta', 2),
|
|
304
|
-
('gamma', 3);
|
|
305
|
-
`)
|
|
306
|
-
setupDb.close()
|
|
307
|
-
|
|
308
|
-
// list tables
|
|
309
|
-
const tablesRes = await fetch(`${base}/api/sqlite/tables`)
|
|
310
|
-
expect(tablesRes.status).toBe(200)
|
|
311
|
-
const tables = await tablesRes.json()
|
|
312
|
-
expect(tables.tables.some((t: any) => t.name === 'widgets')).toBe(true)
|
|
313
|
-
|
|
314
|
-
// browse table data
|
|
315
|
-
const browseRes = await fetch(`${base}/api/sqlite/table-data?table=widgets`)
|
|
316
|
-
expect(browseRes.status).toBe(200)
|
|
317
|
-
const browse = await browseRes.json()
|
|
318
|
-
expect(browse.rows.length).toBe(3)
|
|
319
|
-
expect(browse.total).toBe(3)
|
|
320
|
-
|
|
321
|
-
// search
|
|
322
|
-
const searchRes = await fetch(
|
|
323
|
-
`${base}/api/sqlite/table-data?table=widgets&search=beta`
|
|
324
|
-
)
|
|
325
|
-
const search = await searchRes.json()
|
|
326
|
-
expect(search.rows.length).toBe(1)
|
|
327
|
-
expect(search.rows[0].label).toBe('beta')
|
|
328
|
-
|
|
329
|
-
// raw query
|
|
330
|
-
const queryRes = await fetch(`${base}/api/sqlite/query`, {
|
|
331
|
-
method: 'POST',
|
|
332
|
-
headers: { 'Content-Type': 'application/json' },
|
|
333
|
-
body: JSON.stringify({ sql: 'SELECT count(*) as c FROM widgets' }),
|
|
334
|
-
})
|
|
335
|
-
expect(queryRes.status).toBe(200)
|
|
336
|
-
const q = await queryRes.json()
|
|
337
|
-
expect(q.rows[0].c).toBe(3)
|
|
338
|
-
expect(q.fields).toContain('c')
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
// --- CORS ---
|
|
342
|
-
|
|
343
|
-
test('OPTIONS returns CORS headers', async () => {
|
|
344
|
-
const res = await fetch(`${base}/api/db/tables`, { method: 'OPTIONS' })
|
|
345
|
-
expect(res.status).toBe(200)
|
|
346
|
-
expect(res.headers.get('access-control-allow-origin')).toBe('*')
|
|
347
|
-
})
|
|
348
|
-
})
|
package/src/admin/http-proxy.ts
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { createServer, connect, type Socket, type Server } from 'node:net'
|
|
2
|
-
|
|
3
|
-
import type { ZeroLiteConfig } from '../config.js'
|
|
4
|
-
import type { LogStore } from './log-store.js'
|
|
5
|
-
|
|
6
|
-
export interface HttpLogEntry {
|
|
7
|
-
id: number
|
|
8
|
-
ts: number
|
|
9
|
-
method: string
|
|
10
|
-
path: string
|
|
11
|
-
status: number
|
|
12
|
-
duration: number
|
|
13
|
-
reqSize: number
|
|
14
|
-
resSize: number
|
|
15
|
-
reqHeaders: Record<string, string>
|
|
16
|
-
resHeaders: Record<string, string>
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface HttpLogStore {
|
|
20
|
-
push(entry: Omit<HttpLogEntry, 'id'>): void
|
|
21
|
-
query(opts?: { since?: number; path?: string }): {
|
|
22
|
-
entries: HttpLogEntry[]
|
|
23
|
-
cursor: number
|
|
24
|
-
}
|
|
25
|
-
clear(): void
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const MAX_ENTRIES = 10_000
|
|
29
|
-
const TRIM_BATCH = Math.floor(MAX_ENTRIES * 0.1)
|
|
30
|
-
|
|
31
|
-
export function createHttpLogStore(): HttpLogStore {
|
|
32
|
-
const entries: HttpLogEntry[] = []
|
|
33
|
-
let nextId = 1
|
|
34
|
-
|
|
35
|
-
function push(entry: Omit<HttpLogEntry, 'id'>) {
|
|
36
|
-
const full: HttpLogEntry = { ...entry, id: nextId++ }
|
|
37
|
-
entries.push(full)
|
|
38
|
-
if (entries.length > MAX_ENTRIES + TRIM_BATCH) {
|
|
39
|
-
entries.splice(0, entries.length - MAX_ENTRIES)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function query(opts?: { since?: number; path?: string }) {
|
|
44
|
-
let result: HttpLogEntry[] = entries
|
|
45
|
-
if (opts?.since) {
|
|
46
|
-
const since = opts.since
|
|
47
|
-
let lo = 0
|
|
48
|
-
let hi = result.length
|
|
49
|
-
while (lo < hi) {
|
|
50
|
-
const mid = (lo + hi) >>> 1
|
|
51
|
-
if (result[mid].id <= since) lo = mid + 1
|
|
52
|
-
else hi = mid
|
|
53
|
-
}
|
|
54
|
-
result = result.slice(lo)
|
|
55
|
-
}
|
|
56
|
-
if (opts?.path) {
|
|
57
|
-
const p = opts.path
|
|
58
|
-
result = result.filter((e) => e.path.includes(p))
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
entries: result,
|
|
62
|
-
cursor: entries.length > 0 ? entries[entries.length - 1].id : 0,
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function clear() {
|
|
67
|
-
entries.length = 0
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return { push, query, clear }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function parseHeaders(raw: string): Record<string, string> {
|
|
74
|
-
const out: Record<string, string> = {}
|
|
75
|
-
const lines = raw.split('\r\n')
|
|
76
|
-
for (let i = 1; i < lines.length; i++) {
|
|
77
|
-
if (lines[i] === '') break
|
|
78
|
-
const idx = lines[i].indexOf(': ')
|
|
79
|
-
if (idx > 0) {
|
|
80
|
-
out[lines[i].slice(0, idx).toLowerCase()] = lines[i].slice(idx + 2)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return out
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// public API routes served directly by the proxy (read-only, no auth)
|
|
87
|
-
// these are available at the sprite's public URL under /__orez/
|
|
88
|
-
const CORS =
|
|
89
|
-
'Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, OPTIONS\r\nAccess-Control-Allow-Headers: *'
|
|
90
|
-
|
|
91
|
-
function httpResponse(
|
|
92
|
-
status: number,
|
|
93
|
-
body: string,
|
|
94
|
-
contentType = 'application/json'
|
|
95
|
-
): Buffer {
|
|
96
|
-
const headers = `HTTP/1.1 ${status} ${status === 200 ? 'OK' : 'Error'}\r\nContent-Type: ${contentType}\r\nContent-Length: ${Buffer.byteLength(body)}\r\n${CORS}\r\nConnection: close\r\n\r\n`
|
|
97
|
-
return Buffer.from(headers + body)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function handleOrezRoute(
|
|
101
|
-
path: string,
|
|
102
|
-
method: string,
|
|
103
|
-
logStore?: LogStore,
|
|
104
|
-
config?: ZeroLiteConfig,
|
|
105
|
-
startTime?: number
|
|
106
|
-
): Buffer | null {
|
|
107
|
-
if (method === 'OPTIONS') {
|
|
108
|
-
return httpResponse(200, '')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (method !== 'GET') {
|
|
112
|
-
return httpResponse(405, JSON.stringify({ error: 'method not allowed' }))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const url = new URL(path, 'http://localhost')
|
|
116
|
-
const route = url.pathname.replace(/^\/__orez/, '')
|
|
117
|
-
|
|
118
|
-
if (route === '/api/logs' && logStore) {
|
|
119
|
-
const source = url.searchParams.get('source') || undefined
|
|
120
|
-
const level = url.searchParams.get('level') || undefined
|
|
121
|
-
const sinceStr = url.searchParams.get('since')
|
|
122
|
-
const limitStr = url.searchParams.get('limit')
|
|
123
|
-
const since = sinceStr ? Number(sinceStr) : undefined
|
|
124
|
-
const limit = limitStr ? Number(limitStr) : undefined
|
|
125
|
-
return httpResponse(
|
|
126
|
-
200,
|
|
127
|
-
JSON.stringify(logStore.query({ source, level, since, limit }))
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (route === '/api/status' && config) {
|
|
132
|
-
return httpResponse(
|
|
133
|
-
200,
|
|
134
|
-
JSON.stringify({
|
|
135
|
-
uptime: Math.floor((Date.now() - (startTime || Date.now())) / 1000),
|
|
136
|
-
logLevel: config.logLevel,
|
|
137
|
-
sqliteMode: config.disableWasmSqlite ? 'native' : 'wasm',
|
|
138
|
-
})
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return httpResponse(404, JSON.stringify({ error: 'not found' }))
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// raw tcp proxy that avoids bun's broken node:http upgrade handling.
|
|
146
|
-
// bun silently drops socket.write() data in http server upgrade events,
|
|
147
|
-
// so we do everything at the net level instead.
|
|
148
|
-
//
|
|
149
|
-
// intercepts /__orez/* paths to serve read-only API (logs, status)
|
|
150
|
-
// directly without forwarding to zero-cache.
|
|
151
|
-
export function startHttpProxy(opts: {
|
|
152
|
-
listenPort: number
|
|
153
|
-
targetPort: number
|
|
154
|
-
httpLog: HttpLogStore
|
|
155
|
-
logStore?: LogStore
|
|
156
|
-
config?: ZeroLiteConfig
|
|
157
|
-
startTime?: number
|
|
158
|
-
}): Promise<Server> {
|
|
159
|
-
const { listenPort, targetPort, httpLog, logStore, config, startTime } = opts
|
|
160
|
-
|
|
161
|
-
const server = createServer((client: Socket) => {
|
|
162
|
-
const start = Date.now()
|
|
163
|
-
|
|
164
|
-
let logged = false
|
|
165
|
-
let reqMethod = ''
|
|
166
|
-
let reqPath = ''
|
|
167
|
-
let reqHeaders: Record<string, string> = {}
|
|
168
|
-
|
|
169
|
-
// intercept first client chunk to extract request info
|
|
170
|
-
client.once('data', (chunk: Buffer) => {
|
|
171
|
-
const str = chunk.toString('utf8')
|
|
172
|
-
const firstLine = str.split('\r\n')[0] || ''
|
|
173
|
-
const parts = firstLine.split(' ')
|
|
174
|
-
reqMethod = parts[0] || 'GET'
|
|
175
|
-
reqPath = parts[1] || '/'
|
|
176
|
-
reqHeaders = parseHeaders(str)
|
|
177
|
-
|
|
178
|
-
// intercept /__orez/ paths — serve directly, don't forward to zero-cache
|
|
179
|
-
// check char 0 first to skip the startsWith on hot-path sync/ws traffic
|
|
180
|
-
if (
|
|
181
|
-
reqPath.charCodeAt(0) === 47 &&
|
|
182
|
-
reqPath.charCodeAt(1) === 95 &&
|
|
183
|
-
reqPath.startsWith('/__orez/')
|
|
184
|
-
) {
|
|
185
|
-
const response = handleOrezRoute(reqPath, reqMethod, logStore, config, startTime)
|
|
186
|
-
if (response) {
|
|
187
|
-
client.write(response)
|
|
188
|
-
client.end()
|
|
189
|
-
httpLog.push({
|
|
190
|
-
ts: start,
|
|
191
|
-
method: reqMethod,
|
|
192
|
-
path: reqPath,
|
|
193
|
-
status: 200,
|
|
194
|
-
duration: Date.now() - start,
|
|
195
|
-
reqSize: chunk.length,
|
|
196
|
-
resSize: response.length,
|
|
197
|
-
reqHeaders,
|
|
198
|
-
resHeaders: {},
|
|
199
|
-
})
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// forward to zero-cache
|
|
205
|
-
const target = connect(targetPort, '127.0.0.1')
|
|
206
|
-
|
|
207
|
-
target.setKeepAlive(true, 30_000)
|
|
208
|
-
target.setTimeout(0)
|
|
209
|
-
client.setKeepAlive(true, 30_000)
|
|
210
|
-
client.setTimeout(0)
|
|
211
|
-
|
|
212
|
-
target.write(chunk)
|
|
213
|
-
client.pipe(target)
|
|
214
|
-
|
|
215
|
-
// intercept first target chunk to extract response info and log
|
|
216
|
-
target.once('data', (resChunk: Buffer) => {
|
|
217
|
-
const resStr = resChunk.toString('utf8')
|
|
218
|
-
const resFirstLine = resStr.split('\r\n')[0] || ''
|
|
219
|
-
const status = parseInt(resFirstLine.split(' ')[1]) || 0
|
|
220
|
-
const resHeaders = parseHeaders(resStr)
|
|
221
|
-
|
|
222
|
-
if (!logged) {
|
|
223
|
-
logged = true
|
|
224
|
-
httpLog.push({
|
|
225
|
-
ts: start,
|
|
226
|
-
method: status === 101 ? 'WS' : reqMethod,
|
|
227
|
-
path: reqPath,
|
|
228
|
-
status,
|
|
229
|
-
duration: Date.now() - start,
|
|
230
|
-
reqSize: 0,
|
|
231
|
-
resSize: resChunk.length,
|
|
232
|
-
reqHeaders,
|
|
233
|
-
resHeaders,
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
client.write(resChunk)
|
|
238
|
-
target.pipe(client)
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
target.on('error', () => client.destroy())
|
|
242
|
-
client.on('error', () => target.destroy())
|
|
243
|
-
target.on('close', () => client.destroy())
|
|
244
|
-
client.on('close', () => target.destroy())
|
|
245
|
-
})
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
return new Promise((resolve, reject) => {
|
|
249
|
-
server.listen(listenPort, '127.0.0.1', () => resolve(server as any))
|
|
250
|
-
server.on('error', reject)
|
|
251
|
-
})
|
|
252
|
-
}
|