forked from retoor/devplacepy
feat: add admin/internal database API with CRUD, read-only query, and natural-language SQL endpoints
Add a new `/dbapi` router package providing a generic database API over `dataset`, restricted to admin sessions, admin API keys, and the internal gateway key. Includes: - `tables.py`: list all tables and inspect table schemas - `crud.py`: full CRUD operations (GET, POST, PATCH, DELETE) with soft-delete awareness, born-live inserts, `?include_deleted`, `.../restore`, and `?hard=true` purge - `query.py`: validated read-only SELECT execution via sqlglot parsing, classification, and EXPLAIN dry-run; async query jobs with WebSocket streaming via `DbApiJobService` - `nl.py`: natural-language-to-SQL conversion using the platform AI gateway with re-prompting until validation passes Also register `DbApiJobService` and `PubSubService` in the service manager, add `DBAPI_DIR` to config data paths, and force cleartext `http://` connections to HTTP/1.1 in `curl_transport` to fix large request failures against uvicorn's HTTP/1.1-only internal gateway.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
export class PubSubClient {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.ready = false;
|
||||
this.handlers = new Map();
|
||||
this.pending = [];
|
||||
this.backoff = 200;
|
||||
this.maxBackoff = 5000;
|
||||
}
|
||||
|
||||
_url() {
|
||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${protocol}//${location.host}/pubsub/ws`;
|
||||
}
|
||||
|
||||
_connect() {
|
||||
if (this.socket) return;
|
||||
const socket = new WebSocket(this._url());
|
||||
this.socket = socket;
|
||||
socket.addEventListener("open", () => {
|
||||
this.ready = true;
|
||||
this.backoff = 200;
|
||||
for (const topic of this.handlers.keys()) {
|
||||
socket.send(JSON.stringify({ type: "subscribe", topic }));
|
||||
}
|
||||
const queued = this.pending;
|
||||
this.pending = [];
|
||||
queued.forEach((frame) => socket.send(JSON.stringify(frame)));
|
||||
});
|
||||
socket.addEventListener("message", (event) => {
|
||||
let frame;
|
||||
try {
|
||||
frame = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
if (frame.type === "message") this._dispatch(frame);
|
||||
});
|
||||
socket.addEventListener("close", (event) => {
|
||||
this.ready = false;
|
||||
this.socket = null;
|
||||
const delay = event.code === 4013 ? 200 : this.backoff;
|
||||
this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
|
||||
if (this.handlers.size || this.pending.length) {
|
||||
setTimeout(() => this._connect(), delay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_matches(pattern, topic) {
|
||||
if (pattern === topic || pattern === "*") return true;
|
||||
if (pattern.endsWith(".*")) {
|
||||
return topic === pattern.slice(0, -2) || topic.startsWith(pattern.slice(0, -1));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_dispatch(frame) {
|
||||
for (const [pattern, callbacks] of this.handlers.entries()) {
|
||||
if (this._matches(pattern, frame.topic)) {
|
||||
callbacks.forEach((callback) => callback(frame.data, frame.topic));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_send(frame) {
|
||||
if (this.ready && this.socket) {
|
||||
this.socket.send(JSON.stringify(frame));
|
||||
} else {
|
||||
this.pending.push(frame);
|
||||
this._connect();
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(topic, callback) {
|
||||
let callbacks = this.handlers.get(topic);
|
||||
if (!callbacks) {
|
||||
callbacks = new Set();
|
||||
this.handlers.set(topic, callbacks);
|
||||
this._send({ type: "subscribe", topic });
|
||||
}
|
||||
callbacks.add(callback);
|
||||
this._connect();
|
||||
return () => this.unsubscribe(topic, callback);
|
||||
}
|
||||
|
||||
unsubscribe(topic, callback) {
|
||||
const callbacks = this.handlers.get(topic);
|
||||
if (!callbacks) return;
|
||||
callbacks.delete(callback);
|
||||
if (!callbacks.size) {
|
||||
this.handlers.delete(topic);
|
||||
this._send({ type: "unsubscribe", topic });
|
||||
}
|
||||
}
|
||||
|
||||
publish(topic, data) {
|
||||
this._send({ type: "publish", topic, data });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user