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.
Files changed (157) hide show
  1. package/dist/cf-do/worker.d.ts +3 -0
  2. package/dist/cf-do/worker.d.ts.map +1 -1
  3. package/dist/cf-do/worker.js +37 -15
  4. package/dist/cf-do/worker.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -0
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -4
  9. package/src/admin/admin-data.test.ts +0 -348
  10. package/src/admin/http-proxy.ts +0 -252
  11. package/src/admin/log-store.ts +0 -192
  12. package/src/admin/server.ts +0 -471
  13. package/src/admin/ui.ts +0 -1322
  14. package/src/bench/proxy-throughput.bench.ts +0 -343
  15. package/src/bench/serial-mutations.bench.ts +0 -270
  16. package/src/browser.ts +0 -203
  17. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  18. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  19. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  20. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  21. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  22. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  23. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  24. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  25. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  26. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  27. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  28. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  29. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  30. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  31. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  32. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  33. package/src/cf-do/ARCHITECTURE.md +0 -93
  34. package/src/cf-do/CHAT_E2E.md +0 -213
  35. package/src/cf-do/watermark.test.ts +0 -103
  36. package/src/cf-do/watermark.ts +0 -118
  37. package/src/cf-do/worker.ts +0 -1041
  38. package/src/cf-do/wrangler.toml +0 -11
  39. package/src/cf-pglite/README.md +0 -19
  40. package/src/change-tracking.ts +0 -25
  41. package/src/child-process.test.ts +0 -147
  42. package/src/child-process.ts +0 -90
  43. package/src/cli-entry.ts +0 -72
  44. package/src/cli.test.ts +0 -40
  45. package/src/cli.ts +0 -1214
  46. package/src/config.ts +0 -150
  47. package/src/do-sql-tracking.test.ts +0 -19
  48. package/src/do-sql-tracking.ts +0 -19
  49. package/src/index.ts +0 -1215
  50. package/src/integration/integration.test.ts +0 -517
  51. package/src/integration/native-binary.guard.test.ts +0 -13
  52. package/src/integration/native-startup.test.ts +0 -44
  53. package/src/integration/replication-latency.test.ts +0 -428
  54. package/src/integration/restore-live-stress.test.ts +0 -433
  55. package/src/integration/restore-reset.test.ts +0 -400
  56. package/src/integration/restore.test.ts +0 -274
  57. package/src/integration/test-permissions.ts +0 -147
  58. package/src/load-config.ts +0 -46
  59. package/src/log.ts +0 -96
  60. package/src/mutex.ts +0 -47
  61. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  62. package/src/pg-proxy-browser.ts +0 -2022
  63. package/src/pg-proxy-do-backend.test.ts +0 -3890
  64. package/src/pg-proxy-do-backend.ts +0 -7191
  65. package/src/pg-proxy.ts +0 -1087
  66. package/src/pg-sqlite-compiler/README.md +0 -53
  67. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  71. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  72. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  73. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  74. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  75. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  76. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  77. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  78. package/src/pg-sqlite-compiler/index.ts +0 -73
  79. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  80. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  81. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  82. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  83. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  84. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  85. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  86. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  87. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  88. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  89. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  90. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  91. package/src/pg-sqlite-compiler/types.ts +0 -63
  92. package/src/pglite-ipc.test.ts +0 -116
  93. package/src/pglite-ipc.ts +0 -266
  94. package/src/pglite-manager.ts +0 -557
  95. package/src/pglite-web-proxy.test.ts +0 -57
  96. package/src/pglite-web-proxy.ts +0 -221
  97. package/src/pglite-web-worker.ts +0 -152
  98. package/src/pglite-worker-thread.ts +0 -253
  99. package/src/port.ts +0 -25
  100. package/src/process-title.ts +0 -9
  101. package/src/recovery.ts +0 -155
  102. package/src/replication/change-tracker.test.ts +0 -357
  103. package/src/replication/change-tracker.ts +0 -279
  104. package/src/replication/handler.test.ts +0 -511
  105. package/src/replication/handler.ts +0 -1190
  106. package/src/replication/pgoutput-encoder.test.ts +0 -697
  107. package/src/replication/pgoutput-encoder.ts +0 -373
  108. package/src/replication/tcp-replication.test.ts +0 -876
  109. package/src/replication/zero-compat.test.ts +0 -1150
  110. package/src/restore-stress.test.ts +0 -188
  111. package/src/s3-local.ts +0 -203
  112. package/src/shim/hooks.mjs +0 -120
  113. package/src/shim/register.mjs +0 -4
  114. package/src/sqlite-mode/apply-mode.ts +0 -224
  115. package/src/sqlite-mode/index.ts +0 -15
  116. package/src/sqlite-mode/native-binary.ts +0 -89
  117. package/src/sqlite-mode/package-resolve.ts +0 -17
  118. package/src/sqlite-mode/resolve-mode.ts +0 -80
  119. package/src/sqlite-mode/shim-template.ts +0 -159
  120. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  121. package/src/sqlite-mode/types.ts +0 -30
  122. package/src/vite-plugin.ts +0 -67
  123. package/src/wasm-sqlite.test.ts +0 -537
  124. package/src/worker/browser-admin.ts +0 -52
  125. package/src/worker/browser-build-config.test.ts +0 -71
  126. package/src/worker/browser-build-config.ts +0 -109
  127. package/src/worker/browser-embed-admin.test.ts +0 -75
  128. package/src/worker/browser-embed.ts +0 -345
  129. package/src/worker/cf-patches.ts +0 -384
  130. package/src/worker/embed-integration.test.ts +0 -321
  131. package/src/worker/index.ts +0 -138
  132. package/src/worker/shims/fastify.test.ts +0 -255
  133. package/src/worker/shims/fastify.ts +0 -306
  134. package/src/worker/shims/http-service.test.ts +0 -355
  135. package/src/worker/shims/http-service.ts +0 -293
  136. package/src/worker/shims/node-stub.ts +0 -290
  137. package/src/worker/shims/oxfmt.ts +0 -3
  138. package/src/worker/shims/postgres-browser.ts +0 -59
  139. package/src/worker/shims/postgres-socket.test.ts +0 -576
  140. package/src/worker/shims/postgres-socket.ts +0 -310
  141. package/src/worker/shims/postgres.test.ts +0 -364
  142. package/src/worker/shims/postgres.ts +0 -1454
  143. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  144. package/src/worker/shims/sqlite-browser.ts +0 -175
  145. package/src/worker/shims/sqlite.test.ts +0 -786
  146. package/src/worker/shims/sqlite.ts +0 -978
  147. package/src/worker/shims/stream-browser.ts +0 -15
  148. package/src/worker/shims/ws-browser.test.ts +0 -205
  149. package/src/worker/shims/ws-browser.ts +0 -248
  150. package/src/worker/shims/ws.test.ts +0 -288
  151. package/src/worker/shims/ws.ts +0 -467
  152. package/src/worker/shims/zero-process-env.ts +0 -11
  153. package/src/worker/types.ts +0 -75
  154. package/src/worker/worker-integration.test.ts +0 -223
  155. package/src/worker/worker.test.ts +0 -136
  156. package/src/worker/zero-cache-embed-cf.ts +0 -463
  157. 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
- }