orez 0.2.27 → 0.2.29

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.
Files changed (150) hide show
  1. package/package.json +3 -4
  2. package/src/admin/admin-data.test.ts +0 -348
  3. package/src/admin/http-proxy.ts +0 -252
  4. package/src/admin/log-store.ts +0 -192
  5. package/src/admin/server.ts +0 -471
  6. package/src/admin/ui.ts +0 -1322
  7. package/src/bench/proxy-throughput.bench.ts +0 -343
  8. package/src/bench/serial-mutations.bench.ts +0 -270
  9. package/src/browser.ts +0 -203
  10. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  11. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  12. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  13. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  14. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  15. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  16. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  17. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  18. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  19. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  20. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  21. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  22. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  23. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  24. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  25. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  26. package/src/cf-do/ARCHITECTURE.md +0 -93
  27. package/src/cf-do/CHAT_E2E.md +0 -213
  28. package/src/cf-do/watermark.test.ts +0 -103
  29. package/src/cf-do/watermark.ts +0 -118
  30. package/src/cf-do/worker.ts +0 -1041
  31. package/src/cf-do/wrangler.toml +0 -11
  32. package/src/cf-pglite/README.md +0 -19
  33. package/src/change-tracking.ts +0 -25
  34. package/src/child-process.test.ts +0 -147
  35. package/src/child-process.ts +0 -90
  36. package/src/cli-entry.ts +0 -72
  37. package/src/cli.test.ts +0 -40
  38. package/src/cli.ts +0 -1214
  39. package/src/config.ts +0 -150
  40. package/src/do-sql-tracking.test.ts +0 -19
  41. package/src/do-sql-tracking.ts +0 -19
  42. package/src/index.ts +0 -1215
  43. package/src/integration/integration.test.ts +0 -517
  44. package/src/integration/native-binary.guard.test.ts +0 -13
  45. package/src/integration/native-startup.test.ts +0 -44
  46. package/src/integration/replication-latency.test.ts +0 -428
  47. package/src/integration/restore-live-stress.test.ts +0 -433
  48. package/src/integration/restore-reset.test.ts +0 -400
  49. package/src/integration/restore.test.ts +0 -274
  50. package/src/integration/test-permissions.ts +0 -147
  51. package/src/load-config.ts +0 -46
  52. package/src/log.ts +0 -96
  53. package/src/mutex.ts +0 -47
  54. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  55. package/src/pg-proxy-browser.ts +0 -2022
  56. package/src/pg-proxy-do-backend.test.ts +0 -3890
  57. package/src/pg-proxy-do-backend.ts +0 -7191
  58. package/src/pg-proxy.ts +0 -1087
  59. package/src/pg-sqlite-compiler/README.md +0 -53
  60. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  61. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  62. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  63. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  64. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  65. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  66. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  67. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  71. package/src/pg-sqlite-compiler/index.ts +0 -73
  72. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  73. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  74. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  75. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  76. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  77. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  78. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  79. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  80. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  81. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  82. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  83. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  84. package/src/pg-sqlite-compiler/types.ts +0 -63
  85. package/src/pglite-ipc.test.ts +0 -116
  86. package/src/pglite-ipc.ts +0 -266
  87. package/src/pglite-manager.ts +0 -557
  88. package/src/pglite-web-proxy.test.ts +0 -57
  89. package/src/pglite-web-proxy.ts +0 -221
  90. package/src/pglite-web-worker.ts +0 -152
  91. package/src/pglite-worker-thread.ts +0 -253
  92. package/src/port.ts +0 -25
  93. package/src/process-title.ts +0 -9
  94. package/src/recovery.ts +0 -155
  95. package/src/replication/change-tracker.test.ts +0 -357
  96. package/src/replication/change-tracker.ts +0 -279
  97. package/src/replication/handler.test.ts +0 -511
  98. package/src/replication/handler.ts +0 -1190
  99. package/src/replication/pgoutput-encoder.test.ts +0 -697
  100. package/src/replication/pgoutput-encoder.ts +0 -373
  101. package/src/replication/tcp-replication.test.ts +0 -876
  102. package/src/replication/zero-compat.test.ts +0 -1150
  103. package/src/restore-stress.test.ts +0 -188
  104. package/src/s3-local.ts +0 -203
  105. package/src/shim/hooks.mjs +0 -120
  106. package/src/shim/register.mjs +0 -4
  107. package/src/sqlite-mode/apply-mode.ts +0 -224
  108. package/src/sqlite-mode/index.ts +0 -15
  109. package/src/sqlite-mode/native-binary.ts +0 -89
  110. package/src/sqlite-mode/package-resolve.ts +0 -17
  111. package/src/sqlite-mode/resolve-mode.ts +0 -80
  112. package/src/sqlite-mode/shim-template.ts +0 -159
  113. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  114. package/src/sqlite-mode/types.ts +0 -30
  115. package/src/vite-plugin.ts +0 -67
  116. package/src/wasm-sqlite.test.ts +0 -537
  117. package/src/worker/browser-admin.ts +0 -52
  118. package/src/worker/browser-build-config.test.ts +0 -71
  119. package/src/worker/browser-build-config.ts +0 -109
  120. package/src/worker/browser-embed-admin.test.ts +0 -75
  121. package/src/worker/browser-embed.ts +0 -345
  122. package/src/worker/cf-patches.ts +0 -384
  123. package/src/worker/embed-integration.test.ts +0 -321
  124. package/src/worker/index.ts +0 -138
  125. package/src/worker/shims/fastify.test.ts +0 -255
  126. package/src/worker/shims/fastify.ts +0 -306
  127. package/src/worker/shims/http-service.test.ts +0 -355
  128. package/src/worker/shims/http-service.ts +0 -293
  129. package/src/worker/shims/node-stub.ts +0 -290
  130. package/src/worker/shims/oxfmt.ts +0 -3
  131. package/src/worker/shims/postgres-browser.ts +0 -59
  132. package/src/worker/shims/postgres-socket.test.ts +0 -576
  133. package/src/worker/shims/postgres-socket.ts +0 -310
  134. package/src/worker/shims/postgres.test.ts +0 -364
  135. package/src/worker/shims/postgres.ts +0 -1454
  136. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  137. package/src/worker/shims/sqlite-browser.ts +0 -175
  138. package/src/worker/shims/sqlite.test.ts +0 -786
  139. package/src/worker/shims/sqlite.ts +0 -978
  140. package/src/worker/shims/stream-browser.ts +0 -15
  141. package/src/worker/shims/ws-browser.test.ts +0 -205
  142. package/src/worker/shims/ws-browser.ts +0 -248
  143. package/src/worker/shims/ws.test.ts +0 -288
  144. package/src/worker/shims/ws.ts +0 -467
  145. package/src/worker/shims/zero-process-env.ts +0 -11
  146. package/src/worker/types.ts +0 -75
  147. package/src/worker/worker-integration.test.ts +0 -223
  148. package/src/worker/worker.test.ts +0 -136
  149. package/src/worker/zero-cache-embed-cf.ts +0 -463
  150. package/src/worker/zero-cache-embed.ts +0 -277
@@ -1,192 +0,0 @@
1
- import { mkdirSync, appendFile, stat, rename, unlink, readdirSync } from 'node:fs'
2
- import { join } from 'node:path'
3
-
4
- export interface LogEntry {
5
- id: number
6
- ts: number
7
- source: string
8
- level: string
9
- msg: string
10
- }
11
-
12
- export interface LogStore {
13
- push(source: string, level: string, msg: string): void
14
- query(opts?: { source?: string; level?: string; since?: number; limit?: number }): {
15
- entries: LogEntry[]
16
- cursor: number
17
- }
18
- getAll(): LogEntry[]
19
- clear(): void
20
- }
21
-
22
- const ANSI_RE = /\x1b\[[0-9;]*m/g
23
- const MAX_ENTRIES = 20_000
24
- // trim in batches of 10% to avoid O(n) splice on every single push
25
- const TRIM_BATCH = Math.floor(MAX_ENTRIES * 0.1)
26
- const MAX_FILE_SIZE = 2 * 1024 * 1024
27
- const MAX_QUERY_LIMIT = 5000
28
- const LEVEL_PRIORITY: Record<string, number> = { error: 0, warn: 1, info: 2, debug: 3 }
29
- const VALID_SOURCES = new Set(['orez', 'zero', 'pglite', 'proxy', 's3'])
30
- const VALID_LEVELS = new Set(['error', 'warn', 'info', 'debug'])
31
-
32
- export function createLogStore(
33
- dataDir: string,
34
- writeToDisk = true,
35
- maxFileSize = MAX_FILE_SIZE
36
- ): LogStore {
37
- const entries: LogEntry[] = []
38
- let nextId = 1
39
-
40
- const logsDir = join(dataDir, 'logs')
41
-
42
- if (writeToDisk) {
43
- mkdirSync(logsDir, { recursive: true })
44
- // clean up old rotated log files on startup
45
- try {
46
- for (const f of readdirSync(logsDir)) {
47
- if (/\.log\.\d+$/.test(f)) {
48
- unlink(join(logsDir, f), () => {})
49
- }
50
- }
51
- } catch {}
52
- }
53
-
54
- // track file sizes and rotation state per-source
55
- const fileSizes: Record<string, number> = {}
56
- const rotating: Record<string, boolean> = {}
57
-
58
- // buffered async disk writes — avoids appendFileSync blocking the event loop
59
- const writeBuffers: Record<string, string[]> = {}
60
- const MAX_BUFFER_SIZE = 10_000
61
- const FLUSH_INTERVAL_MS = 3000
62
-
63
- function getLogFile(source: string): string {
64
- return join(logsDir, `${source}.log`)
65
- }
66
-
67
- function rotateIfNeeded(source: string) {
68
- if (!writeToDisk || rotating[source]) return
69
- rotating[source] = true
70
- const logFile = getLogFile(source)
71
- stat(logFile, (err, stats) => {
72
- if (err) {
73
- rotating[source] = false
74
- return
75
- }
76
- fileSizes[source] = stats.size
77
- if (stats.size > maxFileSize) {
78
- // delete old backup first, then rename current
79
- unlink(logFile + '.1', () => {
80
- rename(logFile, logFile + '.1', () => {
81
- fileSizes[source] = 0
82
- rotating[source] = false
83
- })
84
- })
85
- } else {
86
- rotating[source] = false
87
- }
88
- })
89
- }
90
-
91
- function flushBuffers() {
92
- for (const source in writeBuffers) {
93
- const buf = writeBuffers[source]
94
- if (buf.length === 0) continue
95
- const data = buf.join('')
96
- buf.length = 0
97
- const logFile = getLogFile(source)
98
- appendFile(logFile, data, (err) => {
99
- if (err) return
100
- fileSizes[source] = (fileSizes[source] || 0) + data.length
101
- if (fileSizes[source] > maxFileSize) {
102
- rotateIfNeeded(source)
103
- }
104
- })
105
- }
106
- }
107
-
108
- if (writeToDisk) {
109
- const timer = setInterval(flushBuffers, FLUSH_INTERVAL_MS)
110
- if (timer.unref) timer.unref()
111
- }
112
-
113
- function push(source: string, level: string, msg: string) {
114
- const entry: LogEntry = {
115
- id: nextId++,
116
- ts: Date.now(),
117
- source,
118
- level,
119
- msg: msg.replace(ANSI_RE, ''),
120
- }
121
- entries.push(entry)
122
- // trim in batches to amortize the O(n) splice cost — instead of shifting
123
- // 50k elements on every push, we shift once every ~5k pushes
124
- if (entries.length > MAX_ENTRIES + TRIM_BATCH) {
125
- entries.splice(0, entries.length - MAX_ENTRIES)
126
- }
127
- if (writeToDisk) {
128
- const ts = new Date(entry.ts).toISOString()
129
- const line = `[${ts}] [${level}] ${entry.msg}\n`
130
- if (!writeBuffers[source]) writeBuffers[source] = []
131
- const buf = writeBuffers[source]
132
- buf.push(line)
133
- // cap buffer size to prevent unbounded growth if flushBuffers is delayed
134
- if (buf.length > MAX_BUFFER_SIZE) {
135
- buf.splice(0, buf.length - MAX_BUFFER_SIZE)
136
- }
137
- }
138
- }
139
-
140
- function query(opts?: {
141
- source?: string
142
- level?: string
143
- since?: number
144
- limit?: number
145
- }) {
146
- let result = entries
147
- // clamp limit to prevent oversized responses
148
- const limit = Math.min(Math.max(opts?.limit ?? 1000, 1), MAX_QUERY_LIMIT)
149
-
150
- if (opts?.since) {
151
- const since = opts.since
152
- let lo = 0
153
- let hi = result.length
154
- while (lo < hi) {
155
- const mid = (lo + hi) >>> 1
156
- if (result[mid].id <= since) lo = mid + 1
157
- else hi = mid
158
- }
159
- result = result.slice(lo)
160
- }
161
-
162
- if (opts?.source && VALID_SOURCES.has(opts.source)) {
163
- const source = opts.source
164
- result = result.filter((e) => e.source === source)
165
- }
166
-
167
- if (opts?.level && VALID_LEVELS.has(opts.level)) {
168
- const maxPriority = LEVEL_PRIORITY[opts.level] ?? 3
169
- result = result.filter((e) => (LEVEL_PRIORITY[e.level] ?? 3) <= maxPriority)
170
- }
171
-
172
- // limit results to prevent UI slowdown
173
- if (result.length > limit) {
174
- result = result.slice(-limit)
175
- }
176
-
177
- return {
178
- entries: result,
179
- cursor: entries.length > 0 ? entries[entries.length - 1].id : 0,
180
- }
181
- }
182
-
183
- function getAll() {
184
- return [...entries]
185
- }
186
-
187
- function clear() {
188
- entries.length = 0
189
- }
190
-
191
- return { push, query, getAll, clear }
192
- }
@@ -1,471 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import {
3
- createServer,
4
- type Server,
5
- type IncomingMessage,
6
- type ServerResponse,
7
- } from 'node:http'
8
- import { resolve } from 'node:path'
9
-
10
- import { log } from '../log.js'
11
- import { getAdminHtml } from './ui.js'
12
-
13
- import type { ZeroLiteConfig } from '../config.js'
14
- import type { HttpLogStore } from './http-proxy.js'
15
- import type { LogStore } from './log-store.js'
16
- import type { PGlite } from '@electric-sql/pglite'
17
-
18
- export interface AdminActions {
19
- restartZero?: () => Promise<void>
20
- stopZero?: () => Promise<void>
21
- resetZero?: () => Promise<void>
22
- resetZeroFull?: () => Promise<void>
23
- }
24
-
25
- export interface AdminDbInstances {
26
- postgres: PGlite
27
- cvr: PGlite
28
- cdb: PGlite
29
- }
30
-
31
- export interface AdminServerOpts {
32
- port: number
33
- logStore: LogStore
34
- config: ZeroLiteConfig
35
- zeroEnv: Record<string, string>
36
- actions?: AdminActions
37
- startTime: number
38
- httpLog?: HttpLogStore
39
- db?: AdminDbInstances
40
- }
41
-
42
- const CORS_HEADERS: Record<string, string> = {
43
- 'Access-Control-Allow-Origin': '*',
44
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
45
- 'Access-Control-Allow-Headers': '*',
46
- }
47
-
48
- const JSON_HEADERS: Record<string, string> = {
49
- ...CORS_HEADERS,
50
- 'Content-Type': 'application/json',
51
- }
52
-
53
- function json(res: ServerResponse, data: unknown, status = 200) {
54
- res.writeHead(status, JSON_HEADERS)
55
- res.end(JSON.stringify(data))
56
- }
57
-
58
- const UI_PATHS = new Set([
59
- '/',
60
- '/all',
61
- '/data',
62
- '/zero',
63
- '/pglite',
64
- '/proxy',
65
- '/orez',
66
- '/s3',
67
- '/http',
68
- '/env',
69
- ])
70
-
71
- export function startAdminServer(opts: AdminServerOpts): Promise<Server> {
72
- const { logStore, config, zeroEnv, actions, startTime } = opts
73
- const html = getAdminHtml()
74
-
75
- const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
76
- if (req.method === 'OPTIONS') {
77
- res.writeHead(200, CORS_HEADERS)
78
- res.end()
79
- return
80
- }
81
-
82
- const url = new URL(req.url || '/', 'http://localhost:' + opts.port)
83
-
84
- try {
85
- if (req.method === 'GET' && UI_PATHS.has(url.pathname)) {
86
- res.writeHead(200, { ...CORS_HEADERS, 'Content-Type': 'text/html' })
87
- res.end(html)
88
- return
89
- }
90
-
91
- if (req.method === 'GET' && url.pathname === '/api/logs') {
92
- const source = url.searchParams.get('source') || undefined
93
- const level = url.searchParams.get('level') || undefined
94
- const sinceStr = url.searchParams.get('since')
95
- const limitStr = url.searchParams.get('limit')
96
- const since = sinceStr ? Number(sinceStr) : undefined
97
- const limit = limitStr ? Number(limitStr) : undefined
98
- json(res, logStore.query({ source, level, since, limit }))
99
- return
100
- }
101
-
102
- if (req.method === 'GET' && url.pathname === '/api/env') {
103
- const filtered = Object.entries(zeroEnv)
104
- .filter(
105
- ([k]) => k.startsWith('ZERO_') || k === 'NODE_ENV' || k === 'NODE_OPTIONS'
106
- )
107
- .sort(([a], [b]) => a.localeCompare(b))
108
- json(res, { env: Object.fromEntries(filtered) })
109
- return
110
- }
111
-
112
- if (req.method === 'GET' && url.pathname === '/api/status') {
113
- json(res, {
114
- pgPort: config.pgPort,
115
- zeroPort: config.zeroPort,
116
- adminPort: opts.port,
117
- uptime: Math.floor((Date.now() - startTime) / 1000),
118
- logLevel: config.logLevel,
119
- skipZeroCache: config.skipZeroCache,
120
- sqliteMode: config.disableWasmSqlite ? 'native' : 'wasm',
121
- })
122
- return
123
- }
124
-
125
- if (req.method === 'POST' && url.pathname === '/api/actions/restart-zero') {
126
- if (!actions?.restartZero) {
127
- json(res, { ok: false, message: 'zero-cache not running' }, 400)
128
- return
129
- }
130
- log.orez('admin: restarting zero-cache')
131
- await actions.restartZero()
132
- json(res, { ok: true, message: 'zero-cache restarted' })
133
- return
134
- }
135
-
136
- if (req.method === 'POST' && url.pathname === '/api/actions/stop-zero') {
137
- if (!actions?.stopZero) {
138
- json(res, { ok: false, message: 'zero-cache not running' }, 400)
139
- return
140
- }
141
- log.orez('admin: stopping zero-cache for restore')
142
- await actions.stopZero()
143
- json(res, { ok: true, message: 'zero-cache stopped' })
144
- return
145
- }
146
-
147
- if (req.method === 'POST' && url.pathname === '/api/actions/reset-zero') {
148
- if (!actions?.resetZero) {
149
- json(res, { ok: false, message: 'zero-cache not running' }, 400)
150
- return
151
- }
152
- log.orez('admin: resetting zero-cache (cache-only)')
153
- await actions.resetZero()
154
- json(res, { ok: true, message: 'zero-cache reset (cache-only) and restarted' })
155
- return
156
- }
157
-
158
- if (req.method === 'POST' && url.pathname === '/api/actions/reset-zero-full') {
159
- if (!actions?.resetZeroFull) {
160
- json(res, { ok: false, message: 'zero-cache not running' }, 400)
161
- return
162
- }
163
- log.orez('admin: resetting zero-cache (full)')
164
- await actions.resetZeroFull()
165
- json(res, { ok: true, message: 'zero-cache reset (full) and restarted' })
166
- return
167
- }
168
-
169
- if (req.method === 'POST' && url.pathname === '/api/actions/clear-logs') {
170
- logStore.clear()
171
- json(res, { ok: true, message: 'logs cleared' })
172
- return
173
- }
174
-
175
- if (req.method === 'GET' && url.pathname === '/api/http-log') {
176
- const sinceStr = url.searchParams.get('since')
177
- const path = url.searchParams.get('path') || undefined
178
- const since = sinceStr ? Number(sinceStr) : undefined
179
- json(res, opts.httpLog?.query({ since, path }) || { entries: [], cursor: 0 })
180
- return
181
- }
182
-
183
- if (req.method === 'POST' && url.pathname === '/api/actions/clear-http') {
184
- opts.httpLog?.clear()
185
- json(res, { ok: true, message: 'http log cleared' })
186
- return
187
- }
188
-
189
- // db explorer endpoints
190
- if (opts.db && req.method === 'GET' && url.pathname === '/api/db/tables') {
191
- const dbName = url.searchParams.get('db') || 'postgres'
192
- const instance = getDbInstance(opts.db, dbName)
193
- if (!instance) {
194
- json(res, { error: 'unknown db: ' + dbName }, 400)
195
- return
196
- }
197
- try {
198
- const result = await instance.query(
199
- `SELECT table_schema, table_name, pg_total_relation_size(quote_ident(table_schema) || '.' || quote_ident(table_name)) as size_bytes
200
- FROM information_schema.tables
201
- WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
202
- ORDER BY table_schema, table_name`
203
- )
204
- json(res, { tables: result.rows })
205
- } catch (err: any) {
206
- json(res, { error: err?.message ?? 'query failed' }, 500)
207
- }
208
- return
209
- }
210
-
211
- if (opts.db && req.method === 'GET' && url.pathname === '/api/db/table-data') {
212
- const dbName = url.searchParams.get('db') || 'postgres'
213
- const table = url.searchParams.get('table')
214
- if (!table) {
215
- json(res, { error: 'missing table param' }, 400)
216
- return
217
- }
218
- const instance = getDbInstance(opts.db, dbName)
219
- if (!instance) {
220
- json(res, { error: 'unknown db: ' + dbName }, 400)
221
- return
222
- }
223
- const search = url.searchParams.get('search') || ''
224
- const offset = Number(url.searchParams.get('offset') || '0')
225
- const limit = Number(url.searchParams.get('limit') || '100')
226
- try {
227
- // get columns first
228
- const colResult = await instance.query(
229
- `SELECT column_name, data_type FROM information_schema.columns
230
- WHERE table_schema || '.' || table_name = $1 OR table_name = $1
231
- ORDER BY ordinal_position`,
232
- [table]
233
- )
234
- const columns = colResult.rows.map((r: any) => ({
235
- name: r.column_name,
236
- type: r.data_type,
237
- }))
238
- // build query with optional search
239
- let sql = `SELECT * FROM ${quoteIdentPg(table)}`
240
- const params: any[] = []
241
- if (search) {
242
- // search across all text-castable columns
243
- const conds = columns.map(
244
- (_: any, i: number) =>
245
- `${quoteIdentPg(columns[i].name)}::text ILIKE $${params.length + 1}`
246
- )
247
- if (conds.length > 0) {
248
- params.push('%' + search + '%')
249
- sql += ' WHERE ' + conds.join(' OR ')
250
- }
251
- }
252
- // get total count
253
- const countResult = await instance.query(
254
- `SELECT count(*)::int as total FROM (${sql}) _c`,
255
- params
256
- )
257
- const total = (countResult.rows[0] as any)?.total ?? 0
258
- sql += ` LIMIT ${limit} OFFSET ${offset}`
259
- const result = await instance.query(sql, params)
260
- json(res, {
261
- columns,
262
- rows: result.rows,
263
- total,
264
- offset,
265
- limit,
266
- })
267
- } catch (err: any) {
268
- json(res, { error: err?.message ?? 'query failed' }, 500)
269
- }
270
- return
271
- }
272
-
273
- if (opts.db && req.method === 'POST' && url.pathname === '/api/db/query') {
274
- const body = await readBody(req)
275
- let parsed: { db?: string; sql?: string }
276
- try {
277
- parsed = JSON.parse(body)
278
- } catch {
279
- json(res, { error: 'invalid json body' }, 400)
280
- return
281
- }
282
- const dbName = parsed.db || 'postgres'
283
- const sql = parsed.sql
284
- if (!sql) {
285
- json(res, { error: 'missing sql' }, 400)
286
- return
287
- }
288
- const instance = getDbInstance(opts.db, dbName)
289
- if (!instance) {
290
- json(res, { error: 'unknown db: ' + dbName }, 400)
291
- return
292
- }
293
- try {
294
- const start = performance.now()
295
- const result = await instance.query(sql)
296
- const durationMs = Math.round((performance.now() - start) * 100) / 100
297
- json(res, {
298
- fields: (result.fields || []).map((f: any) => f.name),
299
- rows: result.rows,
300
- rowCount: result.rows.length,
301
- durationMs,
302
- })
303
- } catch (err: any) {
304
- json(res, { error: err?.message ?? 'query failed' }, 400)
305
- }
306
- return
307
- }
308
-
309
- // sqlite replica endpoints
310
- if (req.method === 'GET' && url.pathname === '/api/sqlite/tables') {
311
- const sqliteDb = await openSqliteReplica(opts.config.dataDir)
312
- if (!sqliteDb) {
313
- json(res, { error: 'sqlite replica not found' }, 404)
314
- return
315
- }
316
- try {
317
- const tables = sqliteDb
318
- .prepare(
319
- `SELECT name, (SELECT count(*) FROM pragma_table_info(m.name)) as col_count
320
- FROM sqlite_master m WHERE type='table' AND name NOT LIKE 'sqlite_%'
321
- ORDER BY name`
322
- )
323
- .all()
324
- json(res, { tables })
325
- } catch (err: any) {
326
- json(res, { error: err?.message ?? 'query failed' }, 500)
327
- } finally {
328
- sqliteDb.close()
329
- }
330
- return
331
- }
332
-
333
- if (req.method === 'GET' && url.pathname === '/api/sqlite/table-data') {
334
- const table = url.searchParams.get('table')
335
- if (!table) {
336
- json(res, { error: 'missing table param' }, 400)
337
- return
338
- }
339
- const sqliteDb = await openSqliteReplica(opts.config.dataDir)
340
- if (!sqliteDb) {
341
- json(res, { error: 'sqlite replica not found' }, 404)
342
- return
343
- }
344
- const search = url.searchParams.get('search') || ''
345
- const offset = Number(url.searchParams.get('offset') || '0')
346
- const limit = Number(url.searchParams.get('limit') || '100')
347
- try {
348
- const columns = sqliteDb
349
- .prepare(`SELECT name, type FROM pragma_table_info(?)`)
350
- .all(table)
351
- const quotedTable = '"' + table.replace(/"/g, '""') + '"'
352
- let sql = `SELECT * FROM ${quotedTable}`
353
- const params: any[] = []
354
- if (search) {
355
- const conds = columns.map(
356
- (c: any) => `"${c.name.replace(/"/g, '""')}" LIKE ?`
357
- )
358
- if (conds.length > 0) {
359
- params.push(...conds.map(() => '%' + search + '%'))
360
- sql += ' WHERE ' + conds.join(' OR ')
361
- }
362
- }
363
- const countRow = sqliteDb
364
- .prepare(`SELECT count(*) as total FROM (${sql})`)
365
- .get(...params)
366
- const total = (countRow as any)?.total ?? 0
367
- sql += ` LIMIT ? OFFSET ?`
368
- params.push(limit, offset)
369
- const stmt = sqliteDb.prepare(sql)
370
- const rows = stmt.all(...params)
371
- json(res, { columns, rows, total, offset, limit })
372
- } catch (err: any) {
373
- json(res, { error: err?.message ?? 'query failed' }, 500)
374
- } finally {
375
- sqliteDb.close()
376
- }
377
- return
378
- }
379
-
380
- if (req.method === 'POST' && url.pathname === '/api/sqlite/query') {
381
- const body = await readBody(req)
382
- let parsed: { sql?: string }
383
- try {
384
- parsed = JSON.parse(body)
385
- } catch {
386
- json(res, { error: 'invalid json body' }, 400)
387
- return
388
- }
389
- const sql = parsed.sql
390
- if (!sql) {
391
- json(res, { error: 'missing sql' }, 400)
392
- return
393
- }
394
- const sqliteDb = await openSqliteReplica(opts.config.dataDir)
395
- if (!sqliteDb) {
396
- json(res, { error: 'sqlite replica not found' }, 404)
397
- return
398
- }
399
- try {
400
- const start = performance.now()
401
- const stmt = sqliteDb.prepare(sql)
402
- const fields = stmt.columns().map((c: any) => c.name)
403
- const rows = stmt.all()
404
- const durationMs = Math.round((performance.now() - start) * 100) / 100
405
- json(res, { fields, rows, rowCount: rows.length, durationMs })
406
- } catch (err: any) {
407
- json(res, { error: err?.message ?? 'query failed' }, 400)
408
- } finally {
409
- sqliteDb.close()
410
- }
411
- return
412
- }
413
-
414
- res.writeHead(404, CORS_HEADERS)
415
- res.end('not found')
416
- } catch (err: any) {
417
- json(res, { error: err?.message ?? 'internal error' }, 500)
418
- }
419
- })
420
-
421
- return new Promise((resolve, reject) => {
422
- server.listen(opts.port, '127.0.0.1', () => {
423
- resolve(server)
424
- })
425
- server.on('error', reject)
426
- })
427
- }
428
-
429
- function getDbInstance(db: AdminDbInstances, name: string): PGlite | null {
430
- if (name === 'postgres' || name === 'main') return db.postgres
431
- if (name === 'cvr') return db.cvr
432
- if (name === 'cdb') return db.cdb
433
- return null
434
- }
435
-
436
- function readBody(req: IncomingMessage): Promise<string> {
437
- return new Promise((resolve, reject) => {
438
- const chunks: Buffer[] = []
439
- req.on('data', (c) => chunks.push(c))
440
- req.on('end', () => resolve(Buffer.concat(chunks).toString()))
441
- req.on('error', reject)
442
- })
443
- }
444
-
445
- function quoteIdentPg(name: string): string {
446
- if (name.includes('.')) {
447
- return name
448
- .split('.')
449
- .map((p) => '"' + p.replace(/"/g, '""') + '"')
450
- .join('.')
451
- }
452
- if (/^[a-z_][a-z0-9_]*$/.test(name)) return name
453
- return '"' + name.replace(/"/g, '""') + '"'
454
- }
455
-
456
- let cachedDatabaseCtor: any | null = null
457
-
458
- async function openSqliteReplica(dataDir: string): Promise<any | null> {
459
- const replicaPath = resolve(dataDir, 'zero-replica.db')
460
- if (!existsSync(replicaPath)) return null
461
- try {
462
- if (!cachedDatabaseCtor) {
463
- const mod: any = await import('bedrock-sqlite')
464
- cachedDatabaseCtor = mod.Database || mod.default?.Database || mod.default || mod
465
- }
466
- return new cachedDatabaseCtor(replicaPath, { readonly: true })
467
- } catch (err: any) {
468
- log.debug.orez('admin: sqlite replica open failed: ' + (err?.message ?? err))
469
- return null
470
- }
471
- }