@databob/sdk 0.0.0-stage → 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/LICENSE +202 -0
- package/README.md +180 -2
- package/dist/api.d.ts +17 -0
- package/dist/api.js +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +302 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- 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.d.ts +31 -0
- package/dist/project.js +283 -0
- package/dist/react.d.ts +27 -0
- package/dist/react.js +133 -0
- package/dist/server.d.ts +63 -0
- package/dist/server.js +130 -0
- package/package.json +52 -4
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type MetricJson, type ProjectConfig, type SchemaJson } from "@databob/engine";
|
|
2
|
+
export declare const SOURCE_DIR = "databob";
|
|
3
|
+
export declare const STATE_DIR = ".databob";
|
|
4
|
+
export type Paths = {
|
|
5
|
+
root: string;
|
|
6
|
+
source: string;
|
|
7
|
+
generated: string;
|
|
8
|
+
state: string;
|
|
9
|
+
data: string;
|
|
10
|
+
build: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function paths(root: string): Paths;
|
|
13
|
+
export declare function codegen(p: Paths): string[];
|
|
14
|
+
export declare function scaffold(p: Paths): string[];
|
|
15
|
+
export type LoadedProject = {
|
|
16
|
+
schema: SchemaJson;
|
|
17
|
+
metrics: Record<string, MetricJson>;
|
|
18
|
+
problems: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare class ProjectLoadError extends Error {
|
|
21
|
+
}
|
|
22
|
+
export declare function loadProject(p: Paths): Promise<LoadedProject>;
|
|
23
|
+
export type DevState = {
|
|
24
|
+
projectId: string;
|
|
25
|
+
secretKey: string;
|
|
26
|
+
signingKey: string;
|
|
27
|
+
userKey: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function devState(p: Paths): DevState;
|
|
30
|
+
export declare function projectConfig(state: DevState, loaded: LoadedProject): ProjectConfig;
|
|
31
|
+
export declare function writeEnv(p: Paths, url: string, secretKey: string): string[];
|