@databob/sdk 0.0.1 → 0.0.2
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/README.md +24 -2
- package/dist/cli.js +18 -71
- package/dist/mcp.d.ts +163 -0
- package/dist/mcp.js +307 -0
- package/dist/ops.d.ts +33 -0
- package/dist/ops.js +102 -0
- package/dist/project.js +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +4 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -134,7 +134,29 @@ npx databob deploy
|
|
|
134
134
|
|
|
135
135
|
## For agents
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
`databob mcp` is a Model Context Protocol server for coding agents working in your repo. It reads `databob/`, so run it from the project root.
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
claude mcp add databob -- npx databob mcp # Claude Code
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
// .cursor/mcp.json, .vscode/mcp.json, or any client that runs stdio servers
|
|
145
|
+
{ "mcpServers": { "databob": { "command": "npx", "args": ["databob", "mcp"] } } }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Tool | What it does |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| `describe` | Events, properties, metrics, whether a cloud app is configured, and the JSON query language |
|
|
151
|
+
| `check` | Validates `databob/schema.ts` and `databob/metrics.ts` and lists every problem |
|
|
152
|
+
| `query_metric` | Runs a named metric, on local dev data or the cloud app (`target`) |
|
|
153
|
+
| `query` | Runs an ad hoc JSON query, local or cloud |
|
|
154
|
+
| `track_test_event` | Records one event in local dev data only, never the cloud |
|
|
155
|
+
| `deploy` | Ships `databob/` to the cloud app (marked destructive, so clients ask first) |
|
|
156
|
+
|
|
157
|
+
The agent edits the two files with its own tools, runs `check`, tries queries against local data, and deploys when you say so.
|
|
158
|
+
|
|
159
|
+
Every CLI command prints JSON or plain lines too:
|
|
138
160
|
|
|
139
161
|
```sh
|
|
140
162
|
npx databob describe # schema and metrics as JSON
|
|
@@ -144,7 +166,7 @@ npx databob track signup --props '{"plan":"pro"}' --tenant acme --user u1
|
|
|
144
166
|
npx databob deploy # ship databob/ to the cloud app
|
|
145
167
|
```
|
|
146
168
|
|
|
147
|
-
The HTTP API is the same everywhere: `POST /v1/track`, `POST /v1/metrics/:name`, `GET /v1/metrics/:name/live`, `POST /v1/query`, `POST /v1/tokens`, `GET /v1/describe
|
|
169
|
+
The HTTP API is the same everywhere: `POST /v1/track`, `POST /v1/metrics/:name`, `GET /v1/metrics/:name/live`, `POST /v1/query`, `POST /v1/tokens`, `GET /v1/describe` (includes the query language), `DELETE /v1/users/:id`, `DELETE /v1/tenants/:id`. Errors are `{ "error": code, "message": text }`.
|
|
148
170
|
|
|
149
171
|
## Self-host
|
|
150
172
|
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// databob CLI. Every command prints JSON or plain lines, so agents can drive it.
|
|
3
|
-
import {
|
|
3
|
+
import { watch } from "node:fs";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
|
-
import {
|
|
6
|
-
import { parseEnv } from "node:util";
|
|
5
|
+
import { relative } from "node:path";
|
|
7
6
|
import { Engine, EngineError, createHandler } from "@databob/engine";
|
|
8
7
|
import { LocalTenants, StaticProjects, toNodeListener } from "@databob/engine/node";
|
|
9
8
|
import { ProjectLoadError, codegen, devState, loadProject, paths, projectConfig, scaffold, writeEnv, } from "./project.js";
|
|
9
|
+
import { cloudSettings, deployProject, openLocal, trackLocal } from "./ops.js";
|
|
10
|
+
import { serveMcp } from "./mcp.js";
|
|
10
11
|
const HELP = `databob: typed analytics you define in code
|
|
11
12
|
|
|
12
13
|
Usage: databob <command> [options]
|
|
@@ -24,6 +25,8 @@ Usage: databob <command> [options]
|
|
|
24
25
|
--args '{"range":"7d"}' --tenant <id> --tz <IANA zone> --compare
|
|
25
26
|
track <event> [options] Record one event locally
|
|
26
27
|
--props '{"plan":"pro"}' --tenant <id> --user <id>
|
|
28
|
+
mcp Run the MCP server on stdio, for coding agents:
|
|
29
|
+
claude mcp add databob -- npx databob mcp
|
|
27
30
|
|
|
28
31
|
Data lives in .databob/ (git-ignored). Requires Node 22.13+.`;
|
|
29
32
|
function parse(argv) {
|
|
@@ -71,11 +74,6 @@ async function loadOrExit(p) {
|
|
|
71
74
|
}
|
|
72
75
|
return loaded;
|
|
73
76
|
}
|
|
74
|
-
async function local(p) {
|
|
75
|
-
const loaded = await loadOrExit(p);
|
|
76
|
-
const tenants = new LocalTenants({ dir: p.data });
|
|
77
|
-
return { engine: new Engine(tenants), project: projectConfig(devState(p), loaded), tenants };
|
|
78
|
-
}
|
|
79
77
|
const MIN_KEY_LENGTH = 32;
|
|
80
78
|
function requiredSecret(name) {
|
|
81
79
|
const value = process.env[name] ?? "";
|
|
@@ -117,52 +115,17 @@ async function start(p, flags) {
|
|
|
117
115
|
process.on("SIGINT", stop);
|
|
118
116
|
process.on("SIGTERM", stop);
|
|
119
117
|
}
|
|
120
|
-
// Reads .env.local and .env without overriding variables already set.
|
|
121
|
-
function loadEnvFiles(root) {
|
|
122
|
-
for (const name of [".env.local", ".env"]) {
|
|
123
|
-
const file = join(root, name);
|
|
124
|
-
if (!existsSync(file))
|
|
125
|
-
continue;
|
|
126
|
-
const parsed = parseEnv(readFileSync(file, "utf8"));
|
|
127
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
128
|
-
if (process.env[key] === undefined && value !== undefined)
|
|
129
|
-
process.env[key] = value;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
118
|
async function deploy(p, flags) {
|
|
134
|
-
|
|
135
|
-
const url = (str(flags, "url") ?? process.env.DATABOB_URL ?? "").replace(/\/+$/, "");
|
|
136
|
-
const key = process.env.DATABOB_DEPLOY_KEY ?? "";
|
|
137
|
-
if (url === "" || /^https?:\/\/(127\.0\.0\.1|localhost)(:|\/|$)/.test(url)) {
|
|
138
|
-
console.error("✗ Set DATABOB_URL to your databob cloud URL. `databob dev` reloads by itself and needs no deploy.");
|
|
139
|
-
process.exit(1);
|
|
140
|
-
}
|
|
141
|
-
if (!key.startsWith("dbdk_")) {
|
|
142
|
-
console.error("✗ Set DATABOB_DEPLOY_KEY to the app's deploy key (dbdk_...). The dashboard shows it once.");
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
119
|
+
const cloud = cloudSettings(p.root, str(flags, "url"));
|
|
145
120
|
const loaded = await loadOrExit(p);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
151
|
-
body: JSON.stringify({ schema: loaded.schema, metrics: loaded.metrics }),
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
console.error(`✗ Could not reach ${url}: ${error instanceof Error ? error.message : error}`);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
const body = (await response.json().catch(() => ({})));
|
|
159
|
-
if (!response.ok) {
|
|
160
|
-
console.error(`✗ Deploy failed (${response.status}): ${body.message ?? response.statusText}`);
|
|
161
|
-
for (const problem of body.problems ?? [])
|
|
121
|
+
const result = await deployProject(loaded, cloud);
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
console.error(`✗ ${result.status ? `Deploy failed (${result.status}): ` : ""}${result.message}`);
|
|
124
|
+
for (const problem of result.problems ?? [])
|
|
162
125
|
console.error(` ✗ ${problem}`);
|
|
163
126
|
process.exit(1);
|
|
164
127
|
}
|
|
165
|
-
console.log(`✓ Deployed version ${
|
|
128
|
+
console.log(`✓ Deployed version ${result.version}: ${result.events} event(s), ${result.metrics} metric(s) to ${result.url}`);
|
|
166
129
|
}
|
|
167
130
|
async function dev(p, flags) {
|
|
168
131
|
const port = Number(str(flags, "port") ?? process.env.DATABOB_PORT ?? 4100);
|
|
@@ -257,6 +220,7 @@ async function main() {
|
|
|
257
220
|
const created = scaffold(p);
|
|
258
221
|
console.log(created.length > 0 ? `Created ${created.join(", ")}` : "databob/ already exists. Nothing to create.");
|
|
259
222
|
console.log("Next: npx databob dev");
|
|
223
|
+
console.log("Agents: claude mcp add databob -- npx databob mcp");
|
|
260
224
|
return;
|
|
261
225
|
}
|
|
262
226
|
case "dev":
|
|
@@ -265,6 +229,8 @@ async function main() {
|
|
|
265
229
|
return start(p, flags);
|
|
266
230
|
case "deploy":
|
|
267
231
|
return deploy(p, flags);
|
|
232
|
+
case "mcp":
|
|
233
|
+
return serveMcp({ root: process.cwd() });
|
|
268
234
|
case "codegen": {
|
|
269
235
|
const written = codegen(p);
|
|
270
236
|
console.log(written.length > 0 ? `Wrote ${written.join(", ")}` : "Generated files are up to date.");
|
|
@@ -284,7 +250,7 @@ async function main() {
|
|
|
284
250
|
const name = flags._[1];
|
|
285
251
|
if (!name)
|
|
286
252
|
throw new Error("Usage: databob query <metric> [--args JSON] [--tenant id]");
|
|
287
|
-
const { engine, project, tenants } = await
|
|
253
|
+
const { engine, project, tenants } = openLocal(p, await loadOrExit(p));
|
|
288
254
|
const options = {};
|
|
289
255
|
const tenant = str(flags, "tenant");
|
|
290
256
|
if (tenant)
|
|
@@ -309,28 +275,9 @@ async function main() {
|
|
|
309
275
|
const user = str(flags, "user");
|
|
310
276
|
if (user)
|
|
311
277
|
event.user = user;
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
const port = Number(str(flags, "port") ?? process.env.DATABOB_PORT ?? 4100);
|
|
315
|
-
try {
|
|
316
|
-
const response = await fetch(`http://127.0.0.1:${port}/v1/track`, {
|
|
317
|
-
method: "POST",
|
|
318
|
-
headers: { authorization: `Bearer ${state.secretKey}`, "content-type": "application/json" },
|
|
319
|
-
body: JSON.stringify(event),
|
|
320
|
-
});
|
|
321
|
-
const result = (await response.json());
|
|
322
|
-
print(result);
|
|
323
|
-
if (!response.ok || (result.rejected?.length ?? 0) > 0)
|
|
324
|
-
process.exit(1);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
catch {
|
|
328
|
-
// No dev server. Write to the local data directly.
|
|
329
|
-
}
|
|
330
|
-
const { engine, project, tenants } = await local(p);
|
|
331
|
-
const result = await engine.track(project, [event]);
|
|
278
|
+
const port = str(flags, "port");
|
|
279
|
+
const result = await trackLocal(p, await loadOrExit(p), [event], port ? Number(port) : undefined);
|
|
332
280
|
print(result);
|
|
333
|
-
tenants.close();
|
|
334
281
|
if (result.rejected.length > 0)
|
|
335
282
|
process.exit(1);
|
|
336
283
|
return;
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export declare const PROTOCOL_VERSIONS: readonly ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
2
|
+
type Json = Record<string, unknown>;
|
|
3
|
+
export type McpOptions = {
|
|
4
|
+
root: string;
|
|
5
|
+
fetch?: typeof fetch;
|
|
6
|
+
};
|
|
7
|
+
export declare const TOOLS: readonly [{
|
|
8
|
+
readonly name: "describe";
|
|
9
|
+
readonly title: "Describe the analytics project";
|
|
10
|
+
readonly description: "The events, properties, and metrics defined in databob/, whether a cloud app is configured, and a reference for the JSON query language. Call this first.";
|
|
11
|
+
readonly inputSchema: {
|
|
12
|
+
readonly type: "object";
|
|
13
|
+
readonly properties: {};
|
|
14
|
+
readonly additionalProperties: false;
|
|
15
|
+
};
|
|
16
|
+
readonly annotations: {
|
|
17
|
+
readonly readOnlyHint: true;
|
|
18
|
+
readonly openWorldHint: false;
|
|
19
|
+
};
|
|
20
|
+
}, {
|
|
21
|
+
readonly name: "check";
|
|
22
|
+
readonly title: "Check the schema and metrics";
|
|
23
|
+
readonly description: "Validate databob/schema.ts and databob/metrics.ts. Returns every problem, or ok. Run it after editing either file.";
|
|
24
|
+
readonly inputSchema: {
|
|
25
|
+
readonly type: "object";
|
|
26
|
+
readonly properties: {};
|
|
27
|
+
readonly additionalProperties: false;
|
|
28
|
+
};
|
|
29
|
+
readonly annotations: {
|
|
30
|
+
readonly readOnlyHint: true;
|
|
31
|
+
readonly openWorldHint: false;
|
|
32
|
+
};
|
|
33
|
+
}, {
|
|
34
|
+
readonly name: "query_metric";
|
|
35
|
+
readonly title: "Run a named metric";
|
|
36
|
+
readonly description: "Run a metric defined in databob/metrics.ts and return its rows (or funnel steps).";
|
|
37
|
+
readonly inputSchema: {
|
|
38
|
+
readonly type: "object";
|
|
39
|
+
readonly properties: {
|
|
40
|
+
readonly tenant: {
|
|
41
|
+
readonly type: "string";
|
|
42
|
+
readonly description: "Tenant id. Required when the schema has tenants.";
|
|
43
|
+
};
|
|
44
|
+
readonly timeZone: {
|
|
45
|
+
readonly type: "string";
|
|
46
|
+
readonly description: "IANA time zone for time buckets, e.g. \"America/New_York\". Default UTC.";
|
|
47
|
+
};
|
|
48
|
+
readonly compare: {
|
|
49
|
+
readonly type: "boolean";
|
|
50
|
+
readonly description: "Also return the same-length period just before.";
|
|
51
|
+
};
|
|
52
|
+
readonly target: {
|
|
53
|
+
readonly type: "string";
|
|
54
|
+
readonly enum: readonly ["local", "cloud"];
|
|
55
|
+
readonly description: "local: data recorded by `databob dev` in .databob/. cloud: the deployed app at DATABOB_URL. Default \"local\".";
|
|
56
|
+
};
|
|
57
|
+
readonly metric: {
|
|
58
|
+
readonly type: "string";
|
|
59
|
+
readonly description: "The metric's export name.";
|
|
60
|
+
};
|
|
61
|
+
readonly args: {
|
|
62
|
+
readonly type: "object";
|
|
63
|
+
readonly description: "The metric's arguments, e.g. { \"range\": \"7d\" }.";
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
readonly required: readonly ["metric"];
|
|
67
|
+
readonly additionalProperties: false;
|
|
68
|
+
};
|
|
69
|
+
readonly annotations: {
|
|
70
|
+
readonly readOnlyHint: true;
|
|
71
|
+
readonly openWorldHint: true;
|
|
72
|
+
};
|
|
73
|
+
}, {
|
|
74
|
+
readonly name: "query";
|
|
75
|
+
readonly title: "Run an ad hoc query";
|
|
76
|
+
readonly description: "Run a JSON query that is not a saved metric. describe returns the query language. Use it to explore data or to try a metric before adding it to metrics.ts.";
|
|
77
|
+
readonly inputSchema: {
|
|
78
|
+
readonly type: "object";
|
|
79
|
+
readonly properties: {
|
|
80
|
+
readonly tenant: {
|
|
81
|
+
readonly type: "string";
|
|
82
|
+
readonly description: "Tenant id. Required when the schema has tenants.";
|
|
83
|
+
};
|
|
84
|
+
readonly timeZone: {
|
|
85
|
+
readonly type: "string";
|
|
86
|
+
readonly description: "IANA time zone for time buckets, e.g. \"America/New_York\". Default UTC.";
|
|
87
|
+
};
|
|
88
|
+
readonly compare: {
|
|
89
|
+
readonly type: "boolean";
|
|
90
|
+
readonly description: "Also return the same-length period just before.";
|
|
91
|
+
};
|
|
92
|
+
readonly target: {
|
|
93
|
+
readonly type: "string";
|
|
94
|
+
readonly enum: readonly ["local", "cloud"];
|
|
95
|
+
readonly description: "local: data recorded by `databob dev` in .databob/. cloud: the deployed app at DATABOB_URL. Default \"local\".";
|
|
96
|
+
};
|
|
97
|
+
readonly query: {
|
|
98
|
+
readonly type: "object";
|
|
99
|
+
readonly description: "A query with kind \"aggregate\" or \"funnel\".";
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
readonly required: readonly ["query"];
|
|
103
|
+
readonly additionalProperties: false;
|
|
104
|
+
};
|
|
105
|
+
readonly annotations: {
|
|
106
|
+
readonly readOnlyHint: true;
|
|
107
|
+
readonly openWorldHint: true;
|
|
108
|
+
};
|
|
109
|
+
}, {
|
|
110
|
+
readonly name: "track_test_event";
|
|
111
|
+
readonly title: "Record a test event locally";
|
|
112
|
+
readonly description: "Record one event in local dev data (never the cloud), to check instrumentation or to give a new metric something to show. Refused events come back with the reason.";
|
|
113
|
+
readonly inputSchema: {
|
|
114
|
+
readonly type: "object";
|
|
115
|
+
readonly properties: {
|
|
116
|
+
readonly name: {
|
|
117
|
+
readonly type: "string";
|
|
118
|
+
readonly description: "Event name from the schema.";
|
|
119
|
+
};
|
|
120
|
+
readonly props: {
|
|
121
|
+
readonly type: "object";
|
|
122
|
+
readonly description: "Event properties.";
|
|
123
|
+
};
|
|
124
|
+
readonly tenant: {
|
|
125
|
+
readonly type: "string";
|
|
126
|
+
};
|
|
127
|
+
readonly user: {
|
|
128
|
+
readonly type: "string";
|
|
129
|
+
readonly description: "A user id. Stored as a keyed hash.";
|
|
130
|
+
};
|
|
131
|
+
readonly ts: {
|
|
132
|
+
readonly type: "string";
|
|
133
|
+
readonly description: "ISO time. Default now.";
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
readonly required: readonly ["name"];
|
|
137
|
+
readonly additionalProperties: false;
|
|
138
|
+
};
|
|
139
|
+
readonly annotations: {
|
|
140
|
+
readonly readOnlyHint: false;
|
|
141
|
+
readonly destructiveHint: false;
|
|
142
|
+
readonly idempotentHint: false;
|
|
143
|
+
readonly openWorldHint: false;
|
|
144
|
+
};
|
|
145
|
+
}, {
|
|
146
|
+
readonly name: "deploy";
|
|
147
|
+
readonly title: "Deploy to databob cloud";
|
|
148
|
+
readonly description: "Ship databob/schema.ts and databob/metrics.ts to the cloud app at DATABOB_URL with DATABOB_DEPLOY_KEY. Events not in the new schema are refused from then on. Only deploy when the user asks.";
|
|
149
|
+
readonly inputSchema: {
|
|
150
|
+
readonly type: "object";
|
|
151
|
+
readonly properties: {};
|
|
152
|
+
readonly additionalProperties: false;
|
|
153
|
+
};
|
|
154
|
+
readonly annotations: {
|
|
155
|
+
readonly readOnlyHint: false;
|
|
156
|
+
readonly destructiveHint: true;
|
|
157
|
+
readonly idempotentHint: true;
|
|
158
|
+
readonly openWorldHint: true;
|
|
159
|
+
};
|
|
160
|
+
}];
|
|
161
|
+
export declare function createMcpHandler(options: McpOptions): (message: unknown) => Promise<Json | null>;
|
|
162
|
+
export declare function serveMcp(options: McpOptions, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<void>;
|
|
163
|
+
export {};
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// `databob mcp`: a Model Context Protocol server on stdio, so a coding agent
|
|
2
|
+
// working in this repo can read the schema, check it, query local or cloud
|
|
3
|
+
// data, record test events, and deploy.
|
|
4
|
+
//
|
|
5
|
+
// claude mcp add databob -- npx databob mcp
|
|
6
|
+
//
|
|
7
|
+
// Newline-delimited JSON-RPC 2.0. Nothing but protocol messages is written to
|
|
8
|
+
// stdout; logs go to stderr.
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { createInterface } from "node:readline";
|
|
11
|
+
import { QUERY_LANGUAGE } from "@databob/engine";
|
|
12
|
+
import { ProjectLoadError, loadProject, paths } from "./project.js";
|
|
13
|
+
import { cloudSettings, deployProject, openLocal, trackLocal } from "./ops.js";
|
|
14
|
+
import { Databob } from "./server.js";
|
|
15
|
+
export const PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
16
|
+
const INSTRUCTIONS = `databob is analytics defined in code. databob/schema.ts lists events and their properties; databob/metrics.ts holds named queries.
|
|
17
|
+
Start with describe. Edit those two files with your own file tools, then run check. Query with target "local" for data recorded by \`databob dev\`, or "cloud" for the deployed app. Record test events with track_test_event (local only). Deploy only when the user asks.
|
|
18
|
+
databob refuses personal data: no emails, IP addresses, phone numbers, or names in properties. Pass user ids as the user field; they are hashed.`;
|
|
19
|
+
const READ_OPTIONS = {
|
|
20
|
+
tenant: { type: "string", description: "Tenant id. Required when the schema has tenants." },
|
|
21
|
+
timeZone: { type: "string", description: 'IANA time zone for time buckets, e.g. "America/New_York". Default UTC.' },
|
|
22
|
+
compare: { type: "boolean", description: "Also return the same-length period just before." },
|
|
23
|
+
target: {
|
|
24
|
+
type: "string",
|
|
25
|
+
enum: ["local", "cloud"],
|
|
26
|
+
description: 'local: data recorded by `databob dev` in .databob/. cloud: the deployed app at DATABOB_URL. Default "local".',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
export const TOOLS = [
|
|
30
|
+
{
|
|
31
|
+
name: "describe",
|
|
32
|
+
title: "Describe the analytics project",
|
|
33
|
+
description: "The events, properties, and metrics defined in databob/, whether a cloud app is configured, and a reference for the JSON query language. Call this first.",
|
|
34
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
35
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "check",
|
|
39
|
+
title: "Check the schema and metrics",
|
|
40
|
+
description: "Validate databob/schema.ts and databob/metrics.ts. Returns every problem, or ok. Run it after editing either file.",
|
|
41
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
42
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "query_metric",
|
|
46
|
+
title: "Run a named metric",
|
|
47
|
+
description: "Run a metric defined in databob/metrics.ts and return its rows (or funnel steps).",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
metric: { type: "string", description: "The metric's export name." },
|
|
52
|
+
args: { type: "object", description: 'The metric\'s arguments, e.g. { "range": "7d" }.' },
|
|
53
|
+
...READ_OPTIONS,
|
|
54
|
+
},
|
|
55
|
+
required: ["metric"],
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
},
|
|
58
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "query",
|
|
62
|
+
title: "Run an ad hoc query",
|
|
63
|
+
description: "Run a JSON query that is not a saved metric. describe returns the query language. Use it to explore data or to try a metric before adding it to metrics.ts.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
query: { type: "object", description: 'A query with kind "aggregate" or "funnel".' },
|
|
68
|
+
...READ_OPTIONS,
|
|
69
|
+
},
|
|
70
|
+
required: ["query"],
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
},
|
|
73
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "track_test_event",
|
|
77
|
+
title: "Record a test event locally",
|
|
78
|
+
description: "Record one event in local dev data (never the cloud), to check instrumentation or to give a new metric something to show. Refused events come back with the reason.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
name: { type: "string", description: "Event name from the schema." },
|
|
83
|
+
props: { type: "object", description: "Event properties." },
|
|
84
|
+
tenant: { type: "string" },
|
|
85
|
+
user: { type: "string", description: "A user id. Stored as a keyed hash." },
|
|
86
|
+
ts: { type: "string", description: "ISO time. Default now." },
|
|
87
|
+
},
|
|
88
|
+
required: ["name"],
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
},
|
|
91
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "deploy",
|
|
95
|
+
title: "Deploy to databob cloud",
|
|
96
|
+
description: "Ship databob/schema.ts and databob/metrics.ts to the cloud app at DATABOB_URL with DATABOB_DEPLOY_KEY. Events not in the new schema are refused from then on. Only deploy when the user asks.",
|
|
97
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
98
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
class ToolError extends Error {
|
|
102
|
+
}
|
|
103
|
+
function readOptions(input) {
|
|
104
|
+
const options = {};
|
|
105
|
+
if (input.tenant !== undefined) {
|
|
106
|
+
if (typeof input.tenant !== "string")
|
|
107
|
+
throw new ToolError("tenant must be a string");
|
|
108
|
+
options.tenant = input.tenant;
|
|
109
|
+
}
|
|
110
|
+
if (input.timeZone !== undefined) {
|
|
111
|
+
if (typeof input.timeZone !== "string")
|
|
112
|
+
throw new ToolError("timeZone must be a string");
|
|
113
|
+
options.timeZone = input.timeZone;
|
|
114
|
+
}
|
|
115
|
+
if (input.compare === true)
|
|
116
|
+
options.compare = "previous";
|
|
117
|
+
return options;
|
|
118
|
+
}
|
|
119
|
+
function target(input) {
|
|
120
|
+
if (input.target === undefined)
|
|
121
|
+
return "local";
|
|
122
|
+
if (input.target === "local" || input.target === "cloud")
|
|
123
|
+
return input.target;
|
|
124
|
+
throw new ToolError('target is "local" or "cloud"');
|
|
125
|
+
}
|
|
126
|
+
function objectArg(input, name, required) {
|
|
127
|
+
const value = input[name];
|
|
128
|
+
if (value === undefined) {
|
|
129
|
+
if (required)
|
|
130
|
+
throw new ToolError(`${name} is required`);
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
134
|
+
throw new ToolError(`${name} must be an object`);
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
export function createMcpHandler(options) {
|
|
138
|
+
const p = paths(options.root);
|
|
139
|
+
const version = (() => {
|
|
140
|
+
try {
|
|
141
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return "0.0.0";
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
147
|
+
async function project() {
|
|
148
|
+
return loadProject(p);
|
|
149
|
+
}
|
|
150
|
+
function cloudClient() {
|
|
151
|
+
const cloud = cloudSettings(p.root);
|
|
152
|
+
if (!cloud.url || !cloud.secretKey) {
|
|
153
|
+
throw new ToolError("No cloud app is configured. Set DATABOB_URL and DATABOB_SECRET_KEY (or use target local).");
|
|
154
|
+
}
|
|
155
|
+
const clientOptions = { url: cloud.url, secretKey: cloud.secretKey, strict: true };
|
|
156
|
+
if (options.fetch)
|
|
157
|
+
clientOptions.fetch = options.fetch;
|
|
158
|
+
return new Databob(clientOptions);
|
|
159
|
+
}
|
|
160
|
+
async function run(name, input) {
|
|
161
|
+
switch (name) {
|
|
162
|
+
case "describe": {
|
|
163
|
+
const loaded = await project();
|
|
164
|
+
const cloud = cloudSettings(p.root);
|
|
165
|
+
return {
|
|
166
|
+
schema: loaded.schema,
|
|
167
|
+
metrics: loaded.metrics,
|
|
168
|
+
problems: loaded.problems,
|
|
169
|
+
cloud: { url: cloud.url, secretKey: cloud.secretKey !== null, deployKey: cloud.deployKey !== null },
|
|
170
|
+
files: { schema: "databob/schema.ts", metrics: "databob/metrics.ts" },
|
|
171
|
+
queryLanguage: QUERY_LANGUAGE,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
case "check": {
|
|
175
|
+
const loaded = await project();
|
|
176
|
+
return {
|
|
177
|
+
ok: loaded.problems.length === 0,
|
|
178
|
+
problems: loaded.problems,
|
|
179
|
+
events: Object.keys(loaded.schema.events).length,
|
|
180
|
+
metrics: Object.keys(loaded.metrics).length,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
case "query_metric":
|
|
184
|
+
case "query": {
|
|
185
|
+
const read = readOptions(input);
|
|
186
|
+
const where = target(input);
|
|
187
|
+
if (name === "query_metric" && typeof input.metric !== "string")
|
|
188
|
+
throw new ToolError("metric is required");
|
|
189
|
+
const args = objectArg(input, "args", false) ?? {};
|
|
190
|
+
const query = name === "query" ? objectArg(input, "query", true) : null;
|
|
191
|
+
if (where === "cloud") {
|
|
192
|
+
const client = cloudClient();
|
|
193
|
+
const result = query ? await client.queryJson(query, read) : await client.metric({ name: String(input.metric) }, args, read);
|
|
194
|
+
return { target: "cloud", result };
|
|
195
|
+
}
|
|
196
|
+
const loaded = await project();
|
|
197
|
+
if (loaded.problems.length > 0)
|
|
198
|
+
throw new ToolError(`Fix these first:\n${loaded.problems.join("\n")}`);
|
|
199
|
+
const local = openLocal(p, loaded);
|
|
200
|
+
try {
|
|
201
|
+
const result = query
|
|
202
|
+
? await local.engine.query(local.project, query, read)
|
|
203
|
+
: await local.engine.metric(local.project, String(input.metric), args, read);
|
|
204
|
+
return { target: "local", result };
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
local.tenants.close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
case "track_test_event": {
|
|
211
|
+
if (typeof input.name !== "string")
|
|
212
|
+
throw new ToolError("name is required");
|
|
213
|
+
const event = { name: input.name, props: objectArg(input, "props", false) ?? {} };
|
|
214
|
+
for (const key of ["tenant", "user", "ts"]) {
|
|
215
|
+
if (input[key] !== undefined)
|
|
216
|
+
event[key] = input[key];
|
|
217
|
+
}
|
|
218
|
+
const loaded = await project();
|
|
219
|
+
if (loaded.problems.length > 0)
|
|
220
|
+
throw new ToolError(`Fix these first:\n${loaded.problems.join("\n")}`);
|
|
221
|
+
return await trackLocal(p, loaded, [event]);
|
|
222
|
+
}
|
|
223
|
+
case "deploy": {
|
|
224
|
+
const result = await deployProject(await project(), cloudSettings(p.root), options.fetch);
|
|
225
|
+
if (!result.ok) {
|
|
226
|
+
throw new ToolError(`${result.message}${result.problems ? `\n${result.problems.join("\n")}` : ""}`);
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
default:
|
|
231
|
+
throw new ToolError(`Unknown tool "${name}"`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function callTool(params) {
|
|
235
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
236
|
+
const input = (params.arguments ?? {});
|
|
237
|
+
try {
|
|
238
|
+
const result = await run(name, input);
|
|
239
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], structuredContent: result };
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
const message = error instanceof ToolError || error instanceof ProjectLoadError || error instanceof Error ? error.message : String(error);
|
|
243
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Answers one JSON-RPC message. Returns null for notifications.
|
|
247
|
+
return async function handle(message) {
|
|
248
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) {
|
|
249
|
+
return { jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid request" } };
|
|
250
|
+
}
|
|
251
|
+
const { id, method, params } = message;
|
|
252
|
+
const isNotification = id === undefined;
|
|
253
|
+
const reply = (result) => (isNotification ? null : { jsonrpc: "2.0", id, result });
|
|
254
|
+
const fail = (code, text) => isNotification ? null : { jsonrpc: "2.0", id, error: { code, message: text } };
|
|
255
|
+
switch (method) {
|
|
256
|
+
case "initialize": {
|
|
257
|
+
const requested = typeof params?.protocolVersion === "string" ? params.protocolVersion : "";
|
|
258
|
+
const protocolVersion = PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0];
|
|
259
|
+
return reply({
|
|
260
|
+
protocolVersion,
|
|
261
|
+
capabilities: { tools: { listChanged: false } },
|
|
262
|
+
serverInfo: { name: "databob", title: "databob", version },
|
|
263
|
+
instructions: INSTRUCTIONS,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
case "notifications/initialized":
|
|
267
|
+
case "notifications/cancelled":
|
|
268
|
+
return null;
|
|
269
|
+
case "ping":
|
|
270
|
+
return reply({});
|
|
271
|
+
case "tools/list":
|
|
272
|
+
return reply({ tools: TOOLS });
|
|
273
|
+
case "tools/call":
|
|
274
|
+
return reply(await callTool(params ?? {}));
|
|
275
|
+
default:
|
|
276
|
+
return fail(-32601, `Method not found: ${String(method)}`);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export async function serveMcp(options, input = process.stdin, output = process.stdout) {
|
|
281
|
+
// stdout carries protocol messages only.
|
|
282
|
+
console.log = console.error;
|
|
283
|
+
console.info = console.error;
|
|
284
|
+
console.warn = console.error;
|
|
285
|
+
const handle = createMcpHandler(options);
|
|
286
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
287
|
+
const pending = new Set();
|
|
288
|
+
for await (const line of lines) {
|
|
289
|
+
if (line.trim() === "")
|
|
290
|
+
continue;
|
|
291
|
+
let message;
|
|
292
|
+
try {
|
|
293
|
+
message = JSON.parse(line);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
output.write(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } })}\n`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const job = handle(message).then((response) => {
|
|
300
|
+
if (response)
|
|
301
|
+
output.write(`${JSON.stringify(response)}\n`);
|
|
302
|
+
});
|
|
303
|
+
pending.add(job);
|
|
304
|
+
void job.finally(() => pending.delete(job));
|
|
305
|
+
}
|
|
306
|
+
await Promise.all(pending);
|
|
307
|
+
}
|
package/dist/ops.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Engine, type ProjectConfig, type TrackResult } from "@databob/engine";
|
|
2
|
+
import { LocalTenants } from "@databob/engine/node";
|
|
3
|
+
import { type LoadedProject, type Paths } from "./project.js";
|
|
4
|
+
export declare function loadEnvFiles(root: string): void;
|
|
5
|
+
export declare function isLocalUrl(url: string): boolean;
|
|
6
|
+
export type CloudSettings = {
|
|
7
|
+
url: string | null;
|
|
8
|
+
secretKey: string | null;
|
|
9
|
+
deployKey: string | null;
|
|
10
|
+
};
|
|
11
|
+
export declare function cloudSettings(root: string, urlOverride?: string): CloudSettings;
|
|
12
|
+
export type DeployResult = {
|
|
13
|
+
ok: true;
|
|
14
|
+
version: number;
|
|
15
|
+
events: number;
|
|
16
|
+
metrics: number;
|
|
17
|
+
url: string;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
status: number;
|
|
21
|
+
message: string;
|
|
22
|
+
problems?: string[];
|
|
23
|
+
};
|
|
24
|
+
export declare function deployProject(loaded: LoadedProject, cloud: CloudSettings, fetchImpl?: typeof fetch): Promise<DeployResult>;
|
|
25
|
+
export type LocalData = {
|
|
26
|
+
engine: Engine;
|
|
27
|
+
project: ProjectConfig;
|
|
28
|
+
tenants: LocalTenants;
|
|
29
|
+
};
|
|
30
|
+
export declare function openLocal(p: Paths, loaded: LoadedProject): LocalData;
|
|
31
|
+
export declare function trackLocal(p: Paths, loaded: LoadedProject, events: Record<string, unknown>[], port?: number): Promise<TrackResult & {
|
|
32
|
+
via: "dev server" | "local data";
|
|
33
|
+
}>;
|
package/dist/ops.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Operations shared by the CLI and the MCP server. Nothing here prints: each
|
|
2
|
+
// function returns a result, and the caller decides how to show it.
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parseEnv } from "node:util";
|
|
6
|
+
import { Engine } from "@databob/engine";
|
|
7
|
+
import { LocalTenants } from "@databob/engine/node";
|
|
8
|
+
import { devState, projectConfig } from "./project.js";
|
|
9
|
+
// Reads .env.local and .env without overriding variables already set.
|
|
10
|
+
export function loadEnvFiles(root) {
|
|
11
|
+
for (const name of [".env.local", ".env"]) {
|
|
12
|
+
const file = join(root, name);
|
|
13
|
+
if (!existsSync(file))
|
|
14
|
+
continue;
|
|
15
|
+
const parsed = parseEnv(readFileSync(file, "utf8"));
|
|
16
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
17
|
+
if (process.env[key] === undefined && value !== undefined)
|
|
18
|
+
process.env[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function isLocalUrl(url) {
|
|
23
|
+
return /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/|$)/.test(url);
|
|
24
|
+
}
|
|
25
|
+
// The cloud app this project deploys to, from the environment or .env.local.
|
|
26
|
+
export function cloudSettings(root, urlOverride) {
|
|
27
|
+
loadEnvFiles(root);
|
|
28
|
+
const url = (urlOverride ?? process.env.DATABOB_URL ?? "").replace(/\/+$/, "");
|
|
29
|
+
return {
|
|
30
|
+
url: url === "" || isLocalUrl(url) ? null : url,
|
|
31
|
+
secretKey: process.env.DATABOB_SECRET_KEY?.startsWith("dbsk_") ? process.env.DATABOB_SECRET_KEY : null,
|
|
32
|
+
deployKey: process.env.DATABOB_DEPLOY_KEY?.startsWith("dbdk_") ? process.env.DATABOB_DEPLOY_KEY : null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function deployProject(loaded, cloud, fetchImpl = fetch) {
|
|
36
|
+
if (!cloud.url) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
status: 0,
|
|
40
|
+
message: "Set DATABOB_URL to your databob cloud URL. `databob dev` reloads by itself and needs no deploy.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (!cloud.deployKey) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
status: 0,
|
|
47
|
+
message: "Set DATABOB_DEPLOY_KEY to the app's deploy key (dbdk_...). The dashboard shows it once.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (loaded.problems.length > 0) {
|
|
51
|
+
return { ok: false, status: 0, message: "Fix the schema and metrics first", problems: loaded.problems };
|
|
52
|
+
}
|
|
53
|
+
let response;
|
|
54
|
+
try {
|
|
55
|
+
response = await fetchImpl(`${cloud.url}/v1/deploy`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { authorization: `Bearer ${cloud.deployKey}`, "content-type": "application/json" },
|
|
58
|
+
body: JSON.stringify({ schema: loaded.schema, metrics: loaded.metrics }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return { ok: false, status: 0, message: `Could not reach ${cloud.url}: ${error instanceof Error ? error.message : error}` };
|
|
63
|
+
}
|
|
64
|
+
const body = (await response.json().catch(() => ({})));
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const failed = { ok: false, status: response.status, message: body.message ?? response.statusText };
|
|
67
|
+
if (body.problems)
|
|
68
|
+
failed.problems = body.problems;
|
|
69
|
+
return failed;
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, version: body.version ?? 0, events: body.events ?? 0, metrics: body.metrics ?? 0, url: cloud.url };
|
|
72
|
+
}
|
|
73
|
+
// The local dev data in .databob/data. Close `tenants` when done.
|
|
74
|
+
export function openLocal(p, loaded) {
|
|
75
|
+
const tenants = new LocalTenants({ dir: p.data });
|
|
76
|
+
return { engine: new Engine(tenants), project: projectConfig(devState(p), loaded), tenants };
|
|
77
|
+
}
|
|
78
|
+
// Records events locally. A running `databob dev` takes them first, so live
|
|
79
|
+
// views see them; without one they are written to .databob/data directly.
|
|
80
|
+
export async function trackLocal(p, loaded, events, port = Number(process.env.DATABOB_PORT ?? 4100)) {
|
|
81
|
+
const state = devState(p);
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(`http://127.0.0.1:${port}/v1/track`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { authorization: `Bearer ${state.secretKey}`, "content-type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ events }),
|
|
87
|
+
});
|
|
88
|
+
const result = (await response.json());
|
|
89
|
+
if (Array.isArray(result.rejected))
|
|
90
|
+
return { ...result, via: "dev server" };
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// No dev server.
|
|
94
|
+
}
|
|
95
|
+
const local = openLocal(p, loaded);
|
|
96
|
+
try {
|
|
97
|
+
return { ...(await local.engine.track(local.project, events)), via: "local data" };
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
local.tenants.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/project.js
CHANGED
|
@@ -102,7 +102,7 @@ Rules databob enforces:
|
|
|
102
102
|
- User ids are stored as a keyed hash. \`databob.deleteUser(id)\` erases that user everywhere.
|
|
103
103
|
- Queries are JSON compiled to SQL with bound values. Your query code never runs on the server.
|
|
104
104
|
|
|
105
|
-
Agents: run \`npx databob describe\` for the schema and metrics as JSON, \`npx databob check\` to validate,
|
|
105
|
+
Agents: add the MCP server (\`claude mcp add databob -- npx databob mcp\`), or run \`npx databob describe\` for the schema and metrics as JSON, \`npx databob check\` to validate,
|
|
106
106
|
and \`npx databob query <metric> --args '{"range":"7d"}' --tenant <id>\` to read data.
|
|
107
107
|
`;
|
|
108
108
|
const SERVER_GENERATED = `/* Generated by databob. Do not edit. */
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnySchema, type Built, type EventNames, type EventProps, type QueryRoot, type ResultOf, type TrackResult } from "@databob/engine";
|
|
1
|
+
import { type AnySchema, type Built, type EventNames, type EventProps, type QueryAst, type QueryResult, type QueryRoot, type ResultOf, type TrackResult } from "@databob/engine";
|
|
2
2
|
import type { MetricRef, RefArgs, RefResult } from "./api.js";
|
|
3
3
|
export declare const DEFAULT_URL = "http://127.0.0.1:4100";
|
|
4
4
|
export type TrackContext = {
|
|
@@ -42,6 +42,7 @@ export declare class Databob<S extends AnySchema = AnySchema> {
|
|
|
42
42
|
trackMany(events: TrackInput<S>[]): Promise<TrackResult>;
|
|
43
43
|
metric<R extends MetricRef>(ref: R, args?: RefArgs<R>, options?: ReadOptions): Promise<RefResult<R>>;
|
|
44
44
|
query<B extends Built<unknown>>(build: (q: QueryRoot<S>) => B, options?: ReadOptions): Promise<ResultOf<B>>;
|
|
45
|
+
queryJson(query: QueryAst, options?: ReadOptions): Promise<QueryResult>;
|
|
45
46
|
createToken(options?: {
|
|
46
47
|
tenant?: string;
|
|
47
48
|
expiresIn?: number;
|
package/dist/server.js
CHANGED
|
@@ -102,6 +102,10 @@ export class Databob {
|
|
|
102
102
|
query(build, options = {}) {
|
|
103
103
|
return this.request("POST", "/v1/query", { query: build(queryRoot()).toJSON(), ...options });
|
|
104
104
|
}
|
|
105
|
+
// The same, from a JSON query (for example one an agent wrote).
|
|
106
|
+
queryJson(query, options = {}) {
|
|
107
|
+
return this.request("POST", "/v1/query", { query, ...options });
|
|
108
|
+
}
|
|
105
109
|
// A short-lived token your frontend uses to read one tenant's metrics.
|
|
106
110
|
createToken(options = {}) {
|
|
107
111
|
const body = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@databob/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Privacy-first product analytics you define in code. Typed events, named metrics, per-tenant data, and live React hooks.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"main": "./dist/index.js",
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"bin": {
|
|
16
|
-
"databob": "
|
|
16
|
+
"databob": "dist/cli.js"
|
|
17
17
|
},
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@databob/engine": "^0.0.
|
|
39
|
+
"@databob/engine": "^0.0.2",
|
|
40
40
|
"esbuild": "^0.27.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|