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
package/src/admin/log-store.ts
DELETED
|
@@ -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
|
-
}
|
package/src/admin/server.ts
DELETED
|
@@ -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
|
-
}
|