Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@bonnard/sdk

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bonnard/sdk - npm Package Compare versions

Comparing version
0.4.6
to
0.4.7
+405
-77
dist/ai/tools.js
import { z } from "zod";
// --- Shared helpers ---
const MAX_ROWS = 250;
/**
* Validate that timeDimensions reference actual time-type fields.
* Non-time fields (number, string) in timeDimensions cause Cube to silently
* return unfiltered data — producing wrong results without any error.
* Returns an error object if invalid, or null if OK.
*/
async function validateTimeDimensions(client, timeDims) {
if (!timeDims || timeDims.length === 0)
return null;
const meta = await client.explore({ viewsOnly: false });
for (const td of timeDims) {
const dimName = td.dimension;
const dotIdx = dimName.indexOf(".");
if (dotIdx === -1) {
return {
error: `timeDimension "${dimName}" must be fully qualified (e.g. "orders.created_at").`,
hint: `Use "view_name.field_name" format. Call explore_schema to see available time fields.`,
};
}
const sourceName = dimName.substring(0, dotIdx);
const cube = meta.cubes.find((c) => c.name === sourceName);
if (!cube)
continue;
const field = cube.dimensions.find((d) => d.name === dimName);
if (field && field.type !== "time") {
return {
error: `Cannot use "${dimName}" in timeDimensions — it is type "${field.type}", not "time". Using non-time fields in timeDimensions produces silently wrong results.`,
hint: `Use "${dimName}" in filters instead: { member: "${dimName}", operator: "equals", values: ["value"] }. For numeric year/month columns, use operators like "gte" and "lte" for range filtering.`,
};
}
}
return null;
}
function normalizeValue(val) {

@@ -12,8 +45,2 @@ if (val === null || val === undefined)

}
if (typeof val === "string" && val !== "" && !isNaN(Number(val))) {
const num = Number(val);
if (!Number.isInteger(num))
return Math.round(num * 100) / 100;
return num;
}
return val;

@@ -25,7 +52,17 @@ }

const keys = Object.keys(rows[0]);
// Detect collisions — if two keys strip to the same short name, keep both fully qualified
const shortKeys = keys.map((key) => key.split(".").pop() || key);
const seen = new Set();
const collisions = new Set();
for (const sk of shortKeys) {
if (seen.has(sk))
collisions.add(sk);
seen.add(sk);
}
// Build key mapping: use short key unless it collides
const keyMap = keys.map((key, idx) => [key, collisions.has(shortKeys[idx]) ? key : shortKeys[idx]]);
return rows.map((row) => {
const cleaned = {};
for (const key of keys) {
const shortKey = key.split(".").pop() || key;
cleaned[shortKey] = normalizeValue(row[key]);
for (const [origKey, mappedKey] of keyMap) {
cleaned[mappedKey] = normalizeValue(row[origKey]);
}

@@ -74,3 +111,3 @@ return cleaned;

const timeDimensionObject = z.object({
dimension: z.string().describe("Time dimension (e.g. \"orders.created_at\")"),
dimension: z.string().min(1).describe('Time dimension (e.g. "orders.created_at")'),
granularity: z.enum(["day", "week", "month", "quarter", "year"]).optional().describe("Time granularity for grouping"),

@@ -80,17 +117,43 @@ dateRange: z.array(z.string()).min(2).max(2).optional().describe("Date range as [start, end] in YYYY-MM-DD format"),

const querySchema = z.object({
measures: z.array(z.string()).optional().describe("Measures to query (e.g. [\"orders.revenue\", \"orders.count\"])"),
dimensions: z.array(z.string()).optional().describe("Dimensions to group by (e.g. [\"orders.status\"])"),
timeDimensions: z.array(timeDimensionObject).optional().describe("Time dimensions with date range and optional granularity"),
measures: z.array(z.string()).optional().describe('Measures to query (e.g. ["orders.revenue", "orders.count"])'),
dimensions: z.array(z.string()).optional().describe('Dimensions to group by (e.g. ["orders.status"])'),
timeDimensions: z
.array(timeDimensionObject)
.optional()
.describe("Time dimensions with date range and optional granularity"),
timeDimension: timeDimensionObject.optional().describe("Alias for timeDimensions (single object)"),
filters: z.array(z.object({
member: z.string().optional().describe("Field to filter (e.g. \"orders.status\")"),
filters: z
.array(z.object({
member: z.string().optional().describe('Field to filter (e.g. "orders.status")'),
dimension: z.string().optional().describe("Alias for member"),
operator: z.enum(["equals", "notEquals", "contains", "notContains", "gt", "gte", "lt", "lte", "set", "notSet", "inDateRange", "notInDateRange", "beforeDate", "afterDate"]).describe("Filter operator"),
operator: z
.enum([
"equals",
"notEquals",
"contains",
"notContains",
"gt",
"gte",
"lt",
"lte",
"set",
"notSet",
"inDateRange",
"notInDateRange",
"beforeDate",
"afterDate",
])
.describe("Filter operator"),
values: z.array(z.string()).optional().describe("Values to filter by (not needed for set/notSet operators)"),
})).optional().describe("Filters to apply"),
}))
.optional()
.describe("Filters to apply"),
segments: z.array(z.string()).optional().describe("Pre-defined filter segments"),
order: z.array(z.object({
field: z.string().describe("Field to sort by (e.g. \"orders.revenue\")"),
order: z
.array(z.object({
field: z.string().min(1).describe('Field to sort by (e.g. "orders.revenue")'),
direction: z.enum(["asc", "desc"]).describe("Sort direction"),
})).optional().describe("Sort order"),
}))
.optional()
.describe("Sort order"),
limit: z.number().optional().describe("Maximum rows to return (default: 250, max: 5000)"),

@@ -100,6 +163,6 @@ offset: z.number().optional().describe("Number of rows to skip for pagination"),

const sqlQuerySchema = z.object({
sql: z.string().describe("SQL query using Cube SQL syntax with MEASURE() for aggregations"),
sql: z.string().min(1).describe("SQL query using Cube SQL syntax with MEASURE() for aggregations"),
});
const describeFieldSchema = z.object({
field: z.string().describe("Fully qualified field name (e.g. \"orders.revenue\")"),
field: z.string().min(1).describe('Fully qualified field name (e.g. "orders.revenue")'),
});

@@ -130,3 +193,10 @@ export function createTools(client) {

m.title?.toLowerCase().includes(keyword)) {
results.push({ source: cube.name, sourceType, field: m.name, kind: "measure", type: m.type, description: m.description });
results.push({
source: cube.name,
sourceType,
field: m.name,
kind: "measure",
type: m.type,
description: m.description,
});
}

@@ -140,3 +210,10 @@ }

d.title?.toLowerCase().includes(keyword)) {
results.push({ source: cube.name, sourceType, field: d.name, kind: "dimension", type: d.type, description: d.description });
results.push({
source: cube.name,
sourceType,
field: d.name,
kind: "dimension",
type: d.type,
description: d.description,
});
}

@@ -150,3 +227,10 @@ }

s.title?.toLowerCase().includes(keyword)) {
results.push({ source: cube.name, sourceType, field: s.name, kind: "segment", type: "segment", description: s.description });
results.push({
source: cube.name,
sourceType,
field: s.name,
kind: "segment",
type: "segment",
description: s.description,
});
}

@@ -160,3 +244,5 @@ }

if (!cube) {
return { error: `Source '${args.name}' not found. Available sources: ${cubes.map((c) => c.name).join(", ")}` };
return {
error: `Source '${args.name}' not found. Available sources: ${cubes.map((c) => c.name).join(", ")}`,
};
}

@@ -189,51 +275,67 @@ const dims = cube.dimensions.filter((d) => d.type !== "time");

description: "Query the semantic layer with measures, dimensions, filters, and time dimensions. " +
"All field names must be fully qualified (e.g. \"orders.revenue\"). " +
'All field names must be fully qualified (e.g. "orders.revenue"). ' +
"Use timeDimensions for date range constraints. Results are capped at 250 rows per response. " +
"If data_completeness is \"partial\", use offset to fetch the next page.",
'If data_completeness is "partial", use offset to fetch the next page.',
schema: querySchema,
execute: async (args) => {
// Normalize singular timeDimension → timeDimensions array
const timeDims = args.timeDimensions
|| (args.timeDimension ? [args.timeDimension] : undefined);
// Normalize filter dimension → member
const filters = args.filters?.map((f) => ({
member: f.member || f.dimension,
operator: f.operator,
values: f.values,
})).filter((f) => f.member);
const requestLimit = Math.min(args.limit || MAX_ROWS, 5000);
const cubeQuery = {};
if (args.measures && args.measures.length > 0)
cubeQuery.measures = args.measures;
if (args.dimensions)
cubeQuery.dimensions = args.dimensions;
if (timeDims)
cubeQuery.timeDimensions = timeDims;
if (filters && filters.length > 0)
cubeQuery.filters = filters;
if (args.segments)
cubeQuery.segments = args.segments;
cubeQuery.limit = requestLimit;
if (args.offset)
cubeQuery.offset = args.offset;
if (args.order) {
cubeQuery.order = Object.fromEntries(args.order.map((o) => [o.field, o.direction]));
try {
// Normalize singular timeDimension → timeDimensions array
const timeDims = args.timeDimensions || (args.timeDimension ? [args.timeDimension] : undefined);
// Validate timeDimensions reference actual time-type fields
const timeDimError = await validateTimeDimensions(client, timeDims);
if (timeDimError)
return timeDimError;
// Normalize filter dimension → member
const filters = args.filters
?.map((f) => ({
member: f.member || f.dimension,
operator: f.operator,
values: f.values,
}))
.filter((f) => f.member);
const userLimit = Math.min(args.limit || MAX_ROWS, 5000);
const cubeQuery = {};
if (args.measures && args.measures.length > 0)
cubeQuery.measures = args.measures;
if (args.dimensions)
cubeQuery.dimensions = args.dimensions;
if (timeDims)
cubeQuery.timeDimensions = timeDims;
if (filters && filters.length > 0)
cubeQuery.filters = filters;
if (args.segments)
cubeQuery.segments = args.segments;
cubeQuery.limit = userLimit + 1; // fetch one extra to detect if there's more data
if (args.offset)
cubeQuery.offset = args.offset;
if (args.order) {
cubeQuery.order = Object.fromEntries(args.order.map((o) => [o.field, o.direction]));
}
const result = await client.rawQuery(cubeQuery);
const data = (result.data || []);
if (data.length === 0)
return { data_completeness: "complete", rows_shown: 0, results: [] };
const isPartial = data.length > userLimit;
const capped = data.slice(0, userLimit);
const rows = stripPrefixes(capped);
const response = {
data_completeness: isPartial ? "partial" : "complete",
rows_shown: rows.length,
results: rows,
};
if (isPartial) {
const nextOffset = (args.offset || 0) + rows.length;
response.warning = `Partial results — do not sum or average these rows for totals. Use measures for accurate aggregations. To fetch more rows, use offset: ${nextOffset}.`;
}
response.hint =
"To visualize these results, call visualize_read_me to learn chart options, then call visualize.";
return response;
}
const result = await client.rawQuery(cubeQuery);
const data = (result.data || []);
if (data.length === 0)
return { data_completeness: "complete", rows_shown: 0, results: [] };
const capped = data.slice(0, MAX_ROWS);
const isPartial = data.length > MAX_ROWS || data.length >= requestLimit;
const rows = stripPrefixes(capped);
const response = {
data_completeness: isPartial ? "partial" : "complete",
rows_shown: rows.length,
results: rows,
};
if (isPartial) {
const nextOffset = (args.offset || 0) + rows.length;
response.warning = `Partial results — do not sum or average these rows for totals. Use measures for accurate aggregations. To fetch more rows, use offset: ${nextOffset}.`;
catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
error: message,
hint: "Check field names with explore_schema. Ensure all names are fully qualified (e.g. 'orders.revenue').",
};
}
return response;
},

@@ -267,4 +369,4 @@ };

const message = err instanceof Error ? err.message : String(err);
const hints = generateSqlErrorHints(message, args.sql);
return { error: message, hints };
const hint = generateSqlErrorHints(message, args.sql);
return { error: message, hint };
}

@@ -276,3 +378,3 @@ },

description: "Get detailed metadata for a specific field including its type, description, and which source it belongs to. " +
"The field name must be fully qualified: \"view_name.field_name\" (e.g. \"orders.revenue\").",
'The field name must be fully qualified: "view_name.field_name" (e.g. "orders.revenue").',
schema: describeFieldSchema,

@@ -282,3 +384,3 @@ execute: async (args) => {

if (dotIndex === -1) {
return { error: "Field must be fully qualified (e.g. \"orders.revenue\")" };
return { error: 'Field must be fully qualified (e.g. "orders.revenue")' };
}

@@ -290,3 +392,5 @@ const sourceName = args.field.substring(0, dotIndex);

if (!cube) {
return { error: `Source '${sourceName}' not found. Available sources: ${meta.cubes.map((c) => c.name).join(", ")}` };
return {
error: `Source '${sourceName}' not found. Available sources: ${meta.cubes.map((c) => c.name).join(", ")}`,
};
}

@@ -322,3 +426,10 @@ const sourceType = cube.type === "view" ? "view" : "cube";

if (segment) {
return { name: segment.name, kind: "segment", type: "segment", description: segment.description, source: cube.name, sourceType };
return {
name: segment.name,
kind: "segment",
type: "segment",
description: segment.description,
source: cube.name,
sourceType,
};
}

@@ -328,3 +439,220 @@ return { error: `Field '${fieldName}' not found in source '${sourceName}'` };

};
return [exploreSchema, query, sqlQuery, describeField];
const visualizeReadMe = {
name: "visualize_read_me",
description: "Load visualization capabilities and chart options. Call this before using the visualize tool. " +
"Returns available chart types, tool schema, and examples.",
schema: z.object({}),
execute: async () => {
return {
guide: `# Visualization Guide
## Chart Types
- **line** — time series, trends. Best with timeDimensions.
- **bar** — categorical comparisons. Auto-switches to horizontal when >8 categories.
- **area** — stacked composition over time. Use with stacking: "stacked".
- **pie** — part-of-whole. Best with ≤8 categories. Negative values are filtered out.
- **table** — tabular data. Shows all rows with pagination.
## visualize Tool Schema
\`\`\`
{
chartType: "line" | "bar" | "area" | "pie" | "table" (required)
query: { (required)
measures: ["view.measure_name"],
dimensions: ["view.dimension_name"], // optional
timeDimensions: [{ // optional
dimension: "view.time_field",
granularity: "day" | "week" | "month" | "quarter" | "year",
dateRange: ["2025-01-01", "2025-12-31"] // optional
}],
filters: [{ member: "view.field", operator: "equals", values: ["x"] }],
order: { "view.field": "asc" | "desc" },
limit: 250
}
title: "Chart Title" // optional — shown above chart
stacking: "stacked" | "grouped" | "stacked100" // optional — for bar/area
horizontal: true | false // optional — for bar only
}
\`\`\`
## Examples
**Time series line chart:**
\`\`\`
visualize({
chartType: "line",
query: { measures: ["orders.revenue"], timeDimensions: [{ dimension: "orders.created_at", granularity: "month" }] },
title: "Monthly Revenue"
})
\`\`\`
**Categorical bar chart:**
\`\`\`
visualize({
chartType: "bar",
query: { measures: ["orders.count"], dimensions: ["orders.status"] },
title: "Orders by Status"
})
\`\`\`
**Multi-series (pivoted) area chart:**
\`\`\`
visualize({
chartType: "area",
query: { measures: ["orders.count"], dimensions: ["orders.category"], timeDimensions: [{ dimension: "orders.created_at", granularity: "month" }] },
title: "Orders by Category Over Time",
stacking: "stacked"
})
\`\`\`
## Filter Operators
- **equals** — match specific values: \`{ member: "field", operator: "equals", values: ["a", "b"] }\`
- **notEquals** — exclude specific values (same format)
- **contains** — case-insensitive substring search: \`{ member: "field", operator: "contains", values: ["search"] }\`
- **gt, gte, lt, lte** — numeric comparisons: \`{ member: "field", operator: "gte", values: ["100"] }\`
- **set / notSet** — null checks only, ignores values array: \`{ member: "field", operator: "set" }\` means "is not null"
**Important**: To filter to a list of specific items (e.g. top 10 partners), use \`equals\` with the values array — NOT \`set\`. The \`set\` operator only checks for non-null.
## Tips
- Always test your query with the query tool first to confirm it returns valid data.
- Use fully qualified field names: "view_name.field_name".
- For time series, use timeDimensions (not dimensions) for the time field.
- When charting by category with many values (>10), first identify the top N with the query tool, then pass those names as an \`equals\` filter in the visualize query.
- Currency and percentage formatting is auto-detected from field metadata.
- Null dimension values are labeled "(No value)" automatically.
- Missing time intervals are filled automatically.`,
next_step: "Now call visualize() with chartType and a tested query.",
};
},
};
const visualizeSchema = z.object({
chartType: z.enum(["line", "bar", "area", "pie", "table"]).describe("Chart type to render"),
query: z
.object({
measures: z.array(z.string()).optional().describe('Measures (e.g. ["orders.revenue"])'),
dimensions: z.array(z.string()).optional().describe("Dimensions to group by"),
timeDimensions: z
.array(z.object({
dimension: z.string(),
granularity: z.enum(["day", "week", "month", "quarter", "year"]).optional(),
dateRange: z.tuple([z.string(), z.string()]).optional(),
}))
.optional()
.describe("Time dimensions"),
filters: z
.array(z.object({
member: z.string(),
operator: z.enum([
"equals",
"notEquals",
"contains",
"notContains",
"gt",
"gte",
"lt",
"lte",
"set",
"notSet",
"inDateRange",
"notInDateRange",
"beforeDate",
"afterDate",
]),
values: z.array(z.string()).optional(),
}))
.optional(),
order: z.record(z.enum(["asc", "desc"])).optional(),
limit: z.number().optional(),
})
.describe("Query to execute — same format as the query tool"),
title: z.string().optional().describe("Chart title displayed above the chart"),
stacking: z.enum(["stacked", "grouped", "stacked100"]).optional().describe("Stacking mode for bar/area charts"),
horizontal: z.boolean().optional().describe("Horizontal bars (bar chart only)"),
});
const visualize = {
name: "visualize",
description: "Render an interactive chart from a semantic layer query. " +
"You must have called visualize_read_me earlier in this conversation to learn the options. " +
"You should have tested the query via the query tool first to confirm it returns valid data. " +
"Pass the same query that worked with the query tool.",
schema: visualizeSchema,
execute: async (args) => {
try {
// Normalize query (same as query tool)
const queryArgs = args.query;
const timeDims = queryArgs.timeDimensions;
// Validate timeDimensions reference actual time-type fields
const timeDimError = await validateTimeDimensions(client, timeDims);
if (timeDimError)
return { type: "visualization", ...timeDimError };
const filters = queryArgs.filters?.map((f) => ({
member: f.member,
operator: f.operator,
values: f.values,
}));
const cubeQuery = {};
if (queryArgs.measures?.length)
cubeQuery.measures = queryArgs.measures;
if (queryArgs.dimensions)
cubeQuery.dimensions = queryArgs.dimensions;
if (timeDims)
cubeQuery.timeDimensions = timeDims;
if (filters?.length)
cubeQuery.filters = filters;
if (queryArgs.order)
cubeQuery.order = queryArgs.order;
cubeQuery.limit = Math.min(queryArgs.limit || MAX_ROWS, MAX_ROWS);
// Execute query
const result = await client.rawQuery(cubeQuery);
const data = (result.data || []);
if (data.length === 0) {
return {
type: "visualization",
error: "Query returned no data. Check your filters or try a broader query.",
};
}
// Fetch meta for the primary view
const meta = await client.explore({ viewsOnly: false });
const primaryMeasure = queryArgs.measures?.[0] || "";
const viewName = primaryMeasure.includes(".") ? primaryMeasure.split(".")[0] : "";
const viewMeta = meta.cubes.find((c) => c.name === viewName);
if (!viewMeta) {
return {
type: "visualization",
error: `View '${viewName}' not found in schema.`,
};
}
// Return everything the client needs to call resolve() and render
return {
type: "visualization",
chartType: args.chartType,
title: args.title,
stacking: args.stacking,
horizontal: args.horizontal,
data,
meta: {
name: viewMeta.name,
measures: viewMeta.measures,
dimensions: viewMeta.dimensions,
},
query: {
measures: queryArgs.measures || [],
dimensions: queryArgs.dimensions,
timeDimensions: timeDims,
},
rows: data.length,
};
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
type: "visualization",
error: `Query failed: ${message}`,
hint: "Verify field names with explore_schema. Test the query with the query tool first.",
};
}
},
};
return [exploreSchema, query, sqlQuery, describeField, visualizeReadMe, visualize];
}
+1
-1

@@ -1,2 +0,2 @@

"use strict";var Bonnard=(()=>{var y=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var h=(e,r)=>{for(var o in r)y(e,o,{get:r[o],enumerable:!0})},b=(e,r,o,a)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of T(r))!g.call(e,i)&&i!==o&&y(e,i,{get:()=>r[i],enumerable:!(a=w(r,i))||a.enumerable});return e};var R=e=>b(y({},"__esModule",{value:!0}),e);var x={};h(x,{createClient:()=>d,toCubeQuery:()=>c});function c(e){let r={};return e.measures&&(r.measures=e.measures),e.dimensions&&(r.dimensions=e.dimensions),e.filters&&(r.filters=e.filters.map(o=>({member:o.dimension,operator:o.operator,values:o.values}))),e.timeDimension&&(r.timeDimensions=[{dimension:e.timeDimension.dimension,granularity:e.timeDimension.granularity,dateRange:e.timeDimension.dateRange}]),e.orderBy&&(r.order=Object.entries(e.orderBy).map(([o,a])=>[o,a])),e.limit&&(r.limit=e.limit),r}function Q(e){try{let r=e.split(".");if(r.length!==3)return 0;let o=r[1].replace(/-/g,"+").replace(/_/g,"/"),a=JSON.parse(atob(o));return typeof a.exp=="number"?a.exp*1e3:0}catch{return 0}}var q=6e4;function d(e){let r=e.baseUrl||"https://app.bonnard.dev",o=null,a=0,i=null;async function m(){if(e.apiKey)return e.apiKey;if(e.fetchToken){let n=Date.now();return o&&a-q>n?o:i||(i=e.fetchToken().then(t=>(o=t,a=Q(t),t)).finally(()=>{i=null}),i)}throw new Error("BonnardConfig requires either apiKey or fetchToken")}async function l(n,t){let s=await m(),u=await fetch(`${r}${n}`,{method:"POST",headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:JSON.stringify(t)});if(!u.ok){let f=await u.json().catch(()=>({error:u.statusText}));throw new Error(f.error||"Query failed")}return u.json()}async function p(n){let t=await m(),s=await fetch(`${r}${n}`,{method:"GET",headers:{Authorization:`Bearer ${t}`}});if(!s.ok){let u=await s.json().catch(()=>({error:s.statusText}));throw new Error(u.error||"Request failed")}return s.json()}return{async query(n){let t=c(n),s=await l("/api/cube/query",{query:t});return{data:s.data,annotation:s.annotation}},async rawQuery(n){let t=await l("/api/cube/query",{query:n});return{data:t.data,annotation:t.annotation}},async sql(n){return l("/api/cube/query",{sql:n})},async explore(n){let t=await p("/api/cube/meta");return n?.viewsOnly??!0?{cubes:t.cubes.filter(u=>u.type==="view")}:t},async docs(n){let t=new URLSearchParams;n?.topic?t.set("topic",n.topic):n?.category&&t.set("category",n.category);let s=t.toString();return p(`/api/docs${s?`?${s}`:""}`)}}}return R(x);})();
"use strict";var Bonnard=(()=>{var m=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var T=(e,t)=>{for(var n in t)m(e,n,{get:t[n],enumerable:!0})},R=(e,t,n,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of h(t))!g.call(e,i)&&i!==n&&m(e,i,{get:()=>t[i],enumerable:!(a=b(t,i))||a.enumerable});return e};var x=e=>R(m({},"__esModule",{value:!0}),e);var O={};T(O,{BonnardError:()=>c,createClient:()=>f,toCubeQuery:()=>l});function l(e){let t={};return e.measures&&(t.measures=e.measures),e.dimensions&&(t.dimensions=e.dimensions),e.filters&&(t.filters=e.filters.map(n=>({member:n.dimension,operator:n.operator,values:n.values}))),e.timeDimension&&(t.timeDimensions=[{dimension:e.timeDimension.dimension,granularity:e.timeDimension.granularity,dateRange:e.timeDimension.dateRange}]),e.orderBy&&(t.order=Object.entries(e.orderBy).map(([n,a])=>[n,a])),e.limit&&(t.limit=e.limit),t}var c=class extends Error{constructor(n,a){super(n);this.statusCode=a;this.name="BonnardError"}get retryable(){return this.statusCode===429||this.statusCode>=500}};function Q(e){try{let t=e.split(".");if(t.length!==3)return 0;let n=t[1].replace(/-/g,"+").replace(/_/g,"/"),a=JSON.parse(atob(n));return typeof a.exp=="number"?a.exp*1e3:0}catch{return 0}}var q=6e4;function f(e){let t=e.baseUrl||"https://app.bonnard.dev",n=null,a=0,i=null;async function p(){if(e.apiKey)return e.apiKey;if(e.fetchToken){let s=Date.now();return n&&a-q>s?n:i||(i=e.fetchToken().then(r=>(n=r,a=Q(r),r)).finally(()=>{i=null}),i)}throw new Error("BonnardConfig requires either apiKey or fetchToken")}async function y(s,r){let o=await p(),u=await fetch(`${t}${s}`,{method:"POST",headers:{Authorization:`Bearer ${o}`,"Content-Type":"application/json"},body:JSON.stringify(r)});if(!u.ok){let w=await u.json().catch(()=>({error:u.statusText}));throw new c(w.error||"Query failed",u.status)}return u.json()}async function d(s){let r=await p(),o=await fetch(`${t}${s}`,{method:"GET",headers:{Authorization:`Bearer ${r}`}});if(!o.ok){let u=await o.json().catch(()=>({error:o.statusText}));throw new c(u.error||"Request failed",o.status)}return o.json()}return{async query(s){let r=l(s),o=await y("/api/cube/query",{query:r});return{data:o.data,annotation:o.annotation}},async rawQuery(s){let r=await y("/api/cube/query",{query:s});return{data:r.data,annotation:r.annotation}},async sql(s){return y("/api/cube/query",{sql:s})},async explore(s){let r=await d("/api/cube/meta");return s?.viewsOnly??!0?{cubes:r.cubes.filter(u=>u.type==="view")}:r},async docs(s){let r=new URLSearchParams;s?.topic?r.set("topic",s.topic):s?.category&&r.set("category",s.category);let o=r.toString();return d(`/api/docs${o?`?${o}`:""}`)}}}return x(O);})();
//# sourceMappingURL=bonnard.iife.js.map
{
"version": 3,
"sources": ["../src/browser.ts", "../src/query.ts", "../src/client.ts"],
"sourcesContent": ["export { createClient } from './client.js';\nexport { toCubeQuery } from './query.js';\n", "/**\n * Bonnard SDK \u2014 Query format conversion (zero IO)\n */\n\nimport type { QueryOptions } from './types.js';\n\n/**\n * Convert SDK QueryOptions into a Cube-native query object.\n * All field names must be fully qualified (e.g. \"orders.revenue\").\n */\nexport function toCubeQuery(options: QueryOptions): Record<string, unknown> {\n const cubeQuery: Record<string, unknown> = {};\n\n if (options.measures) {\n cubeQuery.measures = options.measures;\n }\n\n if (options.dimensions) {\n cubeQuery.dimensions = options.dimensions;\n }\n\n if (options.filters) {\n cubeQuery.filters = options.filters.map(f => ({\n member: f.dimension,\n operator: f.operator,\n values: f.values,\n }));\n }\n\n if (options.timeDimension) {\n cubeQuery.timeDimensions = [{\n dimension: options.timeDimension.dimension,\n granularity: options.timeDimension.granularity,\n dateRange: options.timeDimension.dateRange,\n }];\n }\n\n if (options.orderBy) {\n cubeQuery.order = Object.entries(options.orderBy).map(([key, dir]) => [key, dir]);\n }\n\n if (options.limit) {\n cubeQuery.limit = options.limit;\n }\n\n return cubeQuery;\n}\n", "/**\n * Bonnard SDK \u2014 Client for querying semantic layer\n */\n\nimport type {\n BonnardConfig,\n QueryOptions,\n QueryResult,\n SqlResult,\n CubeQuery,\n ExploreMeta,\n ExploreOptions,\n DocsOptions,\n DocsTopicListResult,\n DocsTopicResult,\n} from './types.js';\nimport { toCubeQuery } from './query.js';\n\n/**\n * Parse JWT expiry from the payload (base64url-decoded middle segment).\n * Returns the `exp` claim as a millisecond timestamp, or 0 if unparseable.\n */\nfunction parseJwtExpiry(token: string): number {\n try {\n const parts = token.split('.');\n if (parts.length !== 3) return 0;\n // base64url \u2192 base64 \u2192 decode\n const payload = parts[1]!.replace(/-/g, '+').replace(/_/g, '/');\n const json = JSON.parse(atob(payload));\n return typeof json.exp === 'number' ? json.exp * 1000 : 0;\n } catch {\n return 0;\n }\n}\n\nconst REFRESH_BUFFER_MS = 60_000; // refresh 60s before expiry\n\n/**\n * Create a Bonnard client.\n *\n * When `fetchToken` is provided, the returned token is cached automatically\n * and refreshed 60 seconds before its JWT `exp` claim. Concurrent calls are\n * deduplicated so your callback is never invoked more than once at a time.\n */\nexport function createClient(config: BonnardConfig) {\n const baseUrl = config.baseUrl || 'https://app.bonnard.dev';\n\n // Token cache for fetchToken mode\n let cachedToken: string | null = null;\n let cachedExpiry = 0;\n let pendingFetch: Promise<string> | null = null;\n\n async function getToken(): Promise<string> {\n // Static API key \u2014 return directly\n if (config.apiKey) {\n return config.apiKey;\n }\n\n // Token callback \u2014 cache, refresh, and deduplicate\n if (config.fetchToken) {\n const now = Date.now();\n if (cachedToken && cachedExpiry - REFRESH_BUFFER_MS > now) {\n return cachedToken;\n }\n\n // Deduplicate concurrent calls \u2014 share a single in-flight promise\n if (pendingFetch) return pendingFetch;\n\n pendingFetch = config.fetchToken()\n .then((token) => {\n cachedToken = token;\n cachedExpiry = parseJwtExpiry(token);\n return token;\n })\n .finally(() => {\n pendingFetch = null;\n });\n\n return pendingFetch;\n }\n\n throw new Error('BonnardConfig requires either apiKey or fetchToken');\n }\n\n async function request<T>(endpoint: string, body: unknown): Promise<T> {\n const token = await getToken();\n const res = await fetch(`${baseUrl}${endpoint}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const error = await res.json().catch(() => ({ error: res.statusText }));\n throw new Error(error.error || 'Query failed');\n }\n\n return res.json();\n }\n\n async function requestGet<T>(endpoint: string): Promise<T> {\n const token = await getToken();\n const res = await fetch(`${baseUrl}${endpoint}`, {\n method: 'GET',\n headers: {\n 'Authorization': `Bearer ${token}`,\n },\n });\n\n if (!res.ok) {\n const error = await res.json().catch(() => ({ error: res.statusText }));\n throw new Error(error.error || 'Request failed');\n }\n\n return res.json();\n }\n\n return {\n /**\n * Execute a JSON query against the semantic layer.\n * All field names must be fully qualified (e.g. \"orders.revenue\").\n */\n async query<T = Record<string, unknown>>(options: QueryOptions): Promise<QueryResult<T>> {\n const cubeQuery = toCubeQuery(options);\n\n const result = await request<{ data: T[]; annotation?: QueryResult['annotation'] }>(\n '/api/cube/query',\n { query: cubeQuery }\n );\n\n return { data: result.data, annotation: result.annotation };\n },\n\n /**\n * Execute a raw Cube-native JSON query against the semantic layer.\n * Use this when you already have a Cube API query object.\n */\n async rawQuery<T = Record<string, unknown>>(cubeQuery: CubeQuery): Promise<QueryResult<T>> {\n const result = await request<{ data: T[]; annotation?: QueryResult['annotation'] }>(\n '/api/cube/query',\n { query: cubeQuery }\n );\n\n return { data: result.data, annotation: result.annotation };\n },\n\n /**\n * Execute a SQL query against the semantic layer\n */\n async sql<T = Record<string, unknown>>(query: string): Promise<SqlResult<T>> {\n return request<SqlResult<T>>('/api/cube/query', { sql: query });\n },\n\n /**\n * Discover available cubes, measures, dimensions, and segments.\n * By default returns only views (viewsOnly: true).\n */\n async explore(options?: ExploreOptions): Promise<ExploreMeta> {\n const meta = await requestGet<{ cubes: ExploreMeta['cubes'] }>('/api/cube/meta');\n const viewsOnly = options?.viewsOnly ?? true;\n\n if (viewsOnly) {\n return { cubes: meta.cubes.filter(c => c.type === 'view') };\n }\n\n return meta;\n },\n\n /**\n * Access Bonnard documentation.\n * - `docs()` \u2014 list all topics\n * - `docs({ category: 'dashboards' })` \u2014 list topics in a category\n * - `docs({ topic: 'dashboards.components' })` \u2014 get full markdown content\n */\n async docs(options?: DocsOptions): Promise<DocsTopicListResult | DocsTopicResult> {\n const params = new URLSearchParams();\n if (options?.topic) params.set('topic', options.topic);\n else if (options?.category) params.set('category', options.category);\n const qs = params.toString();\n return requestGet(`/api/docs${qs ? `?${qs}` : ''}`);\n },\n };\n}\n"],
"mappings": "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",
"names": ["browser_exports", "__export", "createClient", "toCubeQuery", "toCubeQuery", "options", "cubeQuery", "f", "key", "dir", "parseJwtExpiry", "token", "parts", "payload", "json", "REFRESH_BUFFER_MS", "createClient", "config", "baseUrl", "cachedToken", "cachedExpiry", "pendingFetch", "getToken", "now", "request", "endpoint", "body", "res", "error", "requestGet", "options", "cubeQuery", "toCubeQuery", "result", "query", "meta", "c", "params", "qs"]
"sourcesContent": ["export { createClient, BonnardError } from \"./client.js\";\nexport { toCubeQuery } from \"./query.js\";\n", "/**\n * Bonnard SDK \u2014 Query format conversion (zero IO)\n */\n\nimport type { QueryOptions } from \"./types.js\";\n\n/**\n * Convert SDK QueryOptions into a Cube-native query object.\n * All field names must be fully qualified (e.g. \"orders.revenue\").\n */\nexport function toCubeQuery(options: QueryOptions): Record<string, unknown> {\n const cubeQuery: Record<string, unknown> = {};\n\n if (options.measures) {\n cubeQuery.measures = options.measures;\n }\n\n if (options.dimensions) {\n cubeQuery.dimensions = options.dimensions;\n }\n\n if (options.filters) {\n cubeQuery.filters = options.filters.map((f) => ({\n member: f.dimension,\n operator: f.operator,\n values: f.values,\n }));\n }\n\n if (options.timeDimension) {\n cubeQuery.timeDimensions = [\n {\n dimension: options.timeDimension.dimension,\n granularity: options.timeDimension.granularity,\n dateRange: options.timeDimension.dateRange,\n },\n ];\n }\n\n if (options.orderBy) {\n cubeQuery.order = Object.entries(options.orderBy).map(([key, dir]) => [key, dir]);\n }\n\n if (options.limit) {\n cubeQuery.limit = options.limit;\n }\n\n return cubeQuery;\n}\n", "/**\n * Bonnard SDK \u2014 Client for querying semantic layer\n */\n\n/**\n * Error class that preserves the HTTP status code from the API.\n * Consumers can distinguish between client errors (400), auth errors (401),\n * and server errors (500) for appropriate handling.\n */\nexport class BonnardError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message);\n this.name = \"BonnardError\";\n }\n\n /** Whether the error is retryable (429, 500, 502, 503, 504) */\n get retryable(): boolean {\n return this.statusCode === 429 || this.statusCode >= 500;\n }\n}\n\nimport type {\n BonnardConfig,\n QueryOptions,\n QueryResult,\n SqlResult,\n CubeQuery,\n ExploreMeta,\n ExploreOptions,\n DocsOptions,\n DocsTopicListResult,\n DocsTopicResult,\n} from \"./types.js\";\nimport { toCubeQuery } from \"./query.js\";\n\n/**\n * Parse JWT expiry from the payload (base64url-decoded middle segment).\n * Returns the `exp` claim as a millisecond timestamp, or 0 if unparseable.\n */\nfunction parseJwtExpiry(token: string): number {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return 0;\n // base64url \u2192 base64 \u2192 decode\n const payload = parts[1]!.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const json = JSON.parse(atob(payload));\n return typeof json.exp === \"number\" ? json.exp * 1000 : 0;\n } catch {\n return 0;\n }\n}\n\nconst REFRESH_BUFFER_MS = 60_000; // refresh 60s before expiry\n\n/**\n * Create a Bonnard client.\n *\n * When `fetchToken` is provided, the returned token is cached automatically\n * and refreshed 60 seconds before its JWT `exp` claim. Concurrent calls are\n * deduplicated so your callback is never invoked more than once at a time.\n */\nexport function createClient(config: BonnardConfig) {\n const baseUrl = config.baseUrl || \"https://app.bonnard.dev\";\n\n // Token cache for fetchToken mode\n let cachedToken: string | null = null;\n let cachedExpiry = 0;\n let pendingFetch: Promise<string> | null = null;\n\n async function getToken(): Promise<string> {\n // Static API key \u2014 return directly\n if (config.apiKey) {\n return config.apiKey;\n }\n\n // Token callback \u2014 cache, refresh, and deduplicate\n if (config.fetchToken) {\n const now = Date.now();\n if (cachedToken && cachedExpiry - REFRESH_BUFFER_MS > now) {\n return cachedToken;\n }\n\n // Deduplicate concurrent calls \u2014 share a single in-flight promise\n if (pendingFetch) return pendingFetch;\n\n pendingFetch = config\n .fetchToken()\n .then((token) => {\n cachedToken = token;\n cachedExpiry = parseJwtExpiry(token);\n return token;\n })\n .finally(() => {\n pendingFetch = null;\n });\n\n return pendingFetch;\n }\n\n throw new Error(\"BonnardConfig requires either apiKey or fetchToken\");\n }\n\n async function request<T>(endpoint: string, body: unknown): Promise<T> {\n const token = await getToken();\n const res = await fetch(`${baseUrl}${endpoint}`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const error = await res.json().catch(() => ({ error: res.statusText }));\n throw new BonnardError(error.error || \"Query failed\", res.status);\n }\n\n return res.json();\n }\n\n async function requestGet<T>(endpoint: string): Promise<T> {\n const token = await getToken();\n const res = await fetch(`${baseUrl}${endpoint}`, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!res.ok) {\n const error = await res.json().catch(() => ({ error: res.statusText }));\n throw new BonnardError(error.error || \"Request failed\", res.status);\n }\n\n return res.json();\n }\n\n return {\n /**\n * Execute a JSON query against the semantic layer.\n * All field names must be fully qualified (e.g. \"orders.revenue\").\n */\n async query<T = Record<string, unknown>>(options: QueryOptions): Promise<QueryResult<T>> {\n const cubeQuery = toCubeQuery(options);\n\n const result = await request<{ data: T[]; annotation?: QueryResult[\"annotation\"] }>(\"/api/cube/query\", {\n query: cubeQuery,\n });\n\n return { data: result.data, annotation: result.annotation };\n },\n\n /**\n * Execute a raw Cube-native JSON query against the semantic layer.\n * Use this when you already have a Cube API query object.\n */\n async rawQuery<T = Record<string, unknown>>(cubeQuery: CubeQuery): Promise<QueryResult<T>> {\n const result = await request<{ data: T[]; annotation?: QueryResult[\"annotation\"] }>(\"/api/cube/query\", {\n query: cubeQuery,\n });\n\n return { data: result.data, annotation: result.annotation };\n },\n\n /**\n * Execute a SQL query against the semantic layer\n */\n async sql<T = Record<string, unknown>>(query: string): Promise<SqlResult<T>> {\n return request<SqlResult<T>>(\"/api/cube/query\", { sql: query });\n },\n\n /**\n * Discover available cubes, measures, dimensions, and segments.\n * By default returns only views (viewsOnly: true).\n */\n async explore(options?: ExploreOptions): Promise<ExploreMeta> {\n const meta = await requestGet<{ cubes: ExploreMeta[\"cubes\"] }>(\"/api/cube/meta\");\n const viewsOnly = options?.viewsOnly ?? true;\n\n if (viewsOnly) {\n return { cubes: meta.cubes.filter((c) => c.type === \"view\") };\n }\n\n return meta;\n },\n\n /**\n * Access Bonnard documentation.\n * - `docs()` \u2014 list all topics\n * - `docs({ category: 'dashboards' })` \u2014 list topics in a category\n * - `docs({ topic: 'dashboards.components' })` \u2014 get full markdown content\n */\n async docs(options?: DocsOptions): Promise<DocsTopicListResult | DocsTopicResult> {\n const params = new URLSearchParams();\n if (options?.topic) params.set(\"topic\", options.topic);\n else if (options?.category) params.set(\"category\", options.category);\n const qs = params.toString();\n return requestGet(`/api/docs${qs ? `?${qs}` : \"\"}`);\n },\n };\n}\n"],
"mappings": "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",
"names": ["browser_exports", "__export", "BonnardError", "createClient", "toCubeQuery", "toCubeQuery", "options", "cubeQuery", "f", "key", "dir", "BonnardError", "message", "statusCode", "parseJwtExpiry", "token", "parts", "payload", "json", "REFRESH_BUFFER_MS", "createClient", "config", "baseUrl", "cachedToken", "cachedExpiry", "pendingFetch", "getToken", "now", "request", "endpoint", "body", "res", "error", "requestGet", "options", "cubeQuery", "toCubeQuery", "result", "query", "meta", "c", "params", "qs"]
}

@@ -1,2 +0,2 @@

export { createClient } from './client.js';
export { toCubeQuery } from './query.js';
export { createClient, BonnardError } from "./client.js";
export { toCubeQuery } from "./query.js";

@@ -1,2 +0,2 @@

export { createClient } from './client.js';
export { toCubeQuery } from './query.js';
export { createClient, BonnardError } from "./client.js";
export { toCubeQuery } from "./query.js";
/**
* Bonnard SDK — Client for querying semantic layer
*/
import type { BonnardConfig, QueryOptions, QueryResult, SqlResult, CubeQuery, ExploreMeta, ExploreOptions, DocsOptions, DocsTopicListResult, DocsTopicResult } from './types.js';
/**
* Error class that preserves the HTTP status code from the API.
* Consumers can distinguish between client errors (400), auth errors (401),
* and server errors (500) for appropriate handling.
*/
export declare class BonnardError extends Error {
readonly statusCode: number;
constructor(message: string, statusCode: number);
/** Whether the error is retryable (429, 500, 502, 503, 504) */
get retryable(): boolean;
}
import type { BonnardConfig, QueryOptions, QueryResult, SqlResult, CubeQuery, ExploreMeta, ExploreOptions, DocsOptions, DocsTopicListResult, DocsTopicResult } from "./types.js";
/**
* Create a Bonnard client.

@@ -7,0 +18,0 @@ *

/**
* Bonnard SDK — Client for querying semantic layer
*/
import { toCubeQuery } from './query.js';
/**
* Error class that preserves the HTTP status code from the API.
* Consumers can distinguish between client errors (400), auth errors (401),
* and server errors (500) for appropriate handling.
*/
export class BonnardError extends Error {
statusCode;
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.name = "BonnardError";
}
/** Whether the error is retryable (429, 500, 502, 503, 504) */
get retryable() {
return this.statusCode === 429 || this.statusCode >= 500;
}
}
import { toCubeQuery } from "./query.js";
/**
* Parse JWT expiry from the payload (base64url-decoded middle segment).

@@ -11,9 +28,9 @@ * Returns the `exp` claim as a millisecond timestamp, or 0 if unparseable.

try {
const parts = token.split('.');
const parts = token.split(".");
if (parts.length !== 3)
return 0;
// base64url → base64 → decode
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const json = JSON.parse(atob(payload));
return typeof json.exp === 'number' ? json.exp * 1000 : 0;
return typeof json.exp === "number" ? json.exp * 1000 : 0;
}

@@ -33,3 +50,3 @@ catch {

export function createClient(config) {
const baseUrl = config.baseUrl || 'https://app.bonnard.dev';
const baseUrl = config.baseUrl || "https://app.bonnard.dev";
// Token cache for fetchToken mode

@@ -53,3 +70,4 @@ let cachedToken = null;

return pendingFetch;
pendingFetch = config.fetchToken()
pendingFetch = config
.fetchToken()
.then((token) => {

@@ -65,3 +83,3 @@ cachedToken = token;

}
throw new Error('BonnardConfig requires either apiKey or fetchToken');
throw new Error("BonnardConfig requires either apiKey or fetchToken");
}

@@ -71,6 +89,6 @@ async function request(endpoint, body) {

const res = await fetch(`${baseUrl}${endpoint}`, {
method: 'POST',
method: "POST",
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},

@@ -81,3 +99,3 @@ body: JSON.stringify(body),

const error = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(error.error || 'Query failed');
throw new BonnardError(error.error || "Query failed", res.status);
}

@@ -89,5 +107,5 @@ return res.json();

const res = await fetch(`${baseUrl}${endpoint}`, {
method: 'GET',
method: "GET",
headers: {
'Authorization': `Bearer ${token}`,
Authorization: `Bearer ${token}`,
},

@@ -97,3 +115,3 @@ });

const error = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(error.error || 'Request failed');
throw new BonnardError(error.error || "Request failed", res.status);
}

@@ -109,3 +127,5 @@ return res.json();

const cubeQuery = toCubeQuery(options);
const result = await request('/api/cube/query', { query: cubeQuery });
const result = await request("/api/cube/query", {
query: cubeQuery,
});
return { data: result.data, annotation: result.annotation };

@@ -118,3 +138,5 @@ },

async rawQuery(cubeQuery) {
const result = await request('/api/cube/query', { query: cubeQuery });
const result = await request("/api/cube/query", {
query: cubeQuery,
});
return { data: result.data, annotation: result.annotation };

@@ -126,3 +148,3 @@ },

async sql(query) {
return request('/api/cube/query', { sql: query });
return request("/api/cube/query", { sql: query });
},

@@ -134,6 +156,6 @@ /**

async explore(options) {
const meta = await requestGet('/api/cube/meta');
const meta = await requestGet("/api/cube/meta");
const viewsOnly = options?.viewsOnly ?? true;
if (viewsOnly) {
return { cubes: meta.cubes.filter(c => c.type === 'view') };
return { cubes: meta.cubes.filter((c) => c.type === "view") };
}

@@ -151,9 +173,9 @@ return meta;

if (options?.topic)
params.set('topic', options.topic);
params.set("topic", options.topic);
else if (options?.category)
params.set('category', options.category);
params.set("category", options.category);
const qs = params.toString();
return requestGet(`/api/docs${qs ? `?${qs}` : ''}`);
return requestGet(`/api/docs${qs ? `?${qs}` : ""}`);
},
};
}

@@ -1,3 +0,3 @@

export { createClient } from './client.js';
export { toCubeQuery } from './query.js';
export type { BonnardConfig, QueryOptions, QueryResult, SqlResult, Filter, TimeDimension, InferQueryResult, CubeQuery, ExploreMeta, CubeMetaItem, CubeFieldMeta, CubeSegmentMeta, ExploreOptions, DocsOptions, DocsTopicSummary, DocsTopicListResult, DocsTopicResult, } from './types.js';
export { createClient, BonnardError } from "./client.js";
export { toCubeQuery } from "./query.js";
export type { BonnardConfig, QueryOptions, QueryResult, SqlResult, Filter, TimeDimension, InferQueryResult, CubeQuery, ExploreMeta, CubeMetaItem, CubeFieldMeta, CubeSegmentMeta, ExploreOptions, DocsOptions, DocsTopicSummary, DocsTopicListResult, DocsTopicResult, } from "./types.js";

@@ -1,2 +0,2 @@

export { createClient } from './client.js';
export { toCubeQuery } from './query.js';
export { createClient, BonnardError } from "./client.js";
export { toCubeQuery } from "./query.js";
/**
* Bonnard SDK — Query format conversion (zero IO)
*/
import type { QueryOptions } from './types.js';
import type { QueryOptions } from "./types.js";
/**

@@ -6,0 +6,0 @@ * Convert SDK QueryOptions into a Cube-native query object.

@@ -17,3 +17,3 @@ /**

if (options.filters) {
cubeQuery.filters = options.filters.map(f => ({
cubeQuery.filters = options.filters.map((f) => ({
member: f.dimension,

@@ -25,7 +25,9 @@ operator: f.operator,

if (options.timeDimension) {
cubeQuery.timeDimensions = [{
cubeQuery.timeDimensions = [
{
dimension: options.timeDimension.dimension,
granularity: options.timeDimension.granularity,
dateRange: options.timeDimension.dateRange,
}];
},
];
}

@@ -32,0 +34,0 @@ if (options.orderBy) {

@@ -26,3 +26,3 @@ /**

timeDimension?: TimeDimension;
orderBy?: Record<string, 'asc' | 'desc'>;
orderBy?: Record<string, "asc" | "desc">;
limit?: number;

@@ -35,3 +35,3 @@ }

dimension: string;
granularity?: 'day' | 'week' | 'month' | 'quarter' | 'year';
granularity?: "day" | "week" | "month" | "quarter" | "year";
dateRange?: string | [string, string];

@@ -45,3 +45,3 @@ }>;

segments?: string[];
order?: Record<string, 'asc' | 'desc'> | Array<[string, 'asc' | 'desc']>;
order?: Record<string, "asc" | "desc"> | Array<[string, "asc" | "desc"]>;
limit?: number;

@@ -52,3 +52,3 @@ offset?: number;

dimension: string;
operator: 'equals' | 'notEquals' | 'contains' | 'gt' | 'gte' | 'lt' | 'lte';
operator: "equals" | "notEquals" | "contains" | "gt" | "gte" | "lt" | "lte";
values: (string | number)[];

@@ -58,3 +58,3 @@ }

dimension: string;
granularity?: 'day' | 'week' | 'month' | 'quarter' | 'year';
granularity?: "day" | "week" | "month" | "quarter" | "year";
dateRange?: string | [string, string];

@@ -61,0 +61,0 @@ }

{
"name": "@bonnard/sdk",
"version": "0.4.6",
"version": "0.4.7",
"description": "Bonnard SDK - query your semantic layer from any JavaScript or TypeScript app",

@@ -5,0 +5,0 @@ "type": "module",

@@ -113,2 +113,19 @@ # @bonnard/sdk

### Error handling
All API errors throw `BonnardError` with the HTTP status code:
```typescript
import { createClient, BonnardError } from '@bonnard/sdk';
try {
const { data } = await bon.query({ measures: ['orders.revenue'] });
} catch (err) {
if (err instanceof BonnardError) {
console.log(err.statusCode); // 401, 400, 500, etc.
console.log(err.retryable); // true for 429 and 5xx
}
}
```
## Links

@@ -115,0 +132,0 @@