@bonnard/sdk
Advanced tools
+405
-77
| 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,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"; |
+2
-2
@@ -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"; |
+12
-1
| /** | ||
| * 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 @@ * |
+44
-22
| /** | ||
| * 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}` : ""}`); | ||
| }, | ||
| }; | ||
| } |
+3
-3
@@ -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"; |
+2
-2
@@ -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
-1
| /** | ||
| * 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. |
+5
-3
@@ -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) { |
+5
-5
@@ -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 @@ } |
+1
-1
| { | ||
| "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", |
+17
-0
@@ -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 @@ |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
66956
32.77%1141
45.17%140
13.82%2
100%