@bonnard/sdk
Advanced tools
| export { createTools } from "./tools.js"; | ||
| export type { BonnardTool, BonnardClient } from "./types.js"; |
| export { createTools } from "./tools.js"; |
| import { DynamicStructuredTool } from "@langchain/core/tools"; | ||
| import type { BonnardClient } from "./types.js"; | ||
| /** | ||
| * Create Bonnard tools for LangChain / LangGraph. | ||
| * | ||
| * Returns a `DynamicStructuredTool[]` so tools can be spread directly: | ||
| * ```ts | ||
| * import { createTools } from "@bonnard/sdk/ai/langchain" | ||
| * const tools = createTools(bonnardClient) | ||
| * const agent = createReactAgent({ llm, tools: [...tools, ...myOtherTools] }) | ||
| * ``` | ||
| */ | ||
| export declare function createTools(client: BonnardClient): DynamicStructuredTool<any, any, unknown, string, string>[]; |
| import { DynamicStructuredTool } from "@langchain/core/tools"; | ||
| import { createTools as createBonnardTools } from "./tools.js"; | ||
| /** | ||
| * Create Bonnard tools for LangChain / LangGraph. | ||
| * | ||
| * Returns a `DynamicStructuredTool[]` so tools can be spread directly: | ||
| * ```ts | ||
| * import { createTools } from "@bonnard/sdk/ai/langchain" | ||
| * const tools = createTools(bonnardClient) | ||
| * const agent = createReactAgent({ llm, tools: [...tools, ...myOtherTools] }) | ||
| * ``` | ||
| */ | ||
| export function createTools(client) { | ||
| const bonnardTools = createBonnardTools(client); | ||
| return bonnardTools.map((t) => new DynamicStructuredTool({ | ||
| name: t.name, | ||
| description: t.description, | ||
| schema: t.schema, | ||
| func: async (args) => JSON.stringify(await t.execute(args)), | ||
| })); | ||
| } |
| import type { BonnardClient, BonnardTool } from "./types.js"; | ||
| export declare function createTools(client: BonnardClient): BonnardTool[]; |
+310
| import { z } from "zod"; | ||
| // --- Shared helpers --- | ||
| const MAX_ROWS = 250; | ||
| function normalizeValue(val) { | ||
| if (val === null || val === undefined) | ||
| return null; | ||
| if (typeof val === "number") { | ||
| if (!Number.isInteger(val)) | ||
| return Math.round(val * 100) / 100; | ||
| return val; | ||
| } | ||
| 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; | ||
| } | ||
| function stripPrefixes(rows) { | ||
| if (rows.length === 0) | ||
| return rows; | ||
| const keys = Object.keys(rows[0]); | ||
| return rows.map((row) => { | ||
| const cleaned = {}; | ||
| for (const key of keys) { | ||
| const shortKey = key.split(".").pop() || key; | ||
| cleaned[shortKey] = normalizeValue(row[key]); | ||
| } | ||
| return cleaned; | ||
| }); | ||
| } | ||
| function generateSqlErrorHints(error, sql) { | ||
| const hints = []; | ||
| if (error.includes("Table or CTE with name") && error.includes("not found")) { | ||
| hints.push("- Table not found: use explore_schema to list available views/cubes"); | ||
| } | ||
| if (error.includes("Invalid identifier")) { | ||
| hints.push("- Invalid column: check field names via explore_schema with `name` parameter"); | ||
| hints.push("- In SQL, use column names without view prefix (e.g. `revenue` not `view.revenue`)"); | ||
| } | ||
| if (error.includes("could not be resolved")) { | ||
| const hasMeasure = /MEASURE\s*\(/i.test(sql); | ||
| const hasGroupBy = /GROUP\s+BY/i.test(sql); | ||
| if (!hasMeasure) { | ||
| hints.push("- Missing MEASURE(): wrap measure columns in MEASURE() function"); | ||
| } | ||
| if (!hasGroupBy) { | ||
| hints.push("- Missing GROUP BY: add GROUP BY for non-aggregated columns"); | ||
| } | ||
| } | ||
| if (error.includes("ParserError")) { | ||
| hints.push("- SQL syntax error: check for typos or missing keywords"); | ||
| hints.push("- Use single quotes for string values: WHERE status = 'completed'"); | ||
| } | ||
| if (/\bJOIN\b/i.test(sql)) { | ||
| hints.push("- JOINs not supported: use UNION to combine results from different views"); | ||
| } | ||
| if (hints.length === 0) { | ||
| hints.push("- Verify table/view names with explore_schema"); | ||
| hints.push("- Use MEASURE() to aggregate measures"); | ||
| hints.push("- Include all non-aggregated columns in GROUP BY"); | ||
| } | ||
| return hints.join("\n"); | ||
| } | ||
| // --- Schemas --- | ||
| const exploreSchemaSchema = z.object({ | ||
| name: z.string().optional().describe("Source name to get full details (e.g. 'orders')"), | ||
| search: z.string().optional().describe("Keyword to search across all field names and descriptions"), | ||
| }); | ||
| const timeDimensionObject = z.object({ | ||
| dimension: z.string().describe("Time dimension (e.g. \"orders.created_at\")"), | ||
| granularity: z.enum(["day", "week", "month", "quarter", "year"]).optional().describe("Time granularity for grouping"), | ||
| dateRange: z.tuple([z.string(), z.string()]).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"), | ||
| 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\")"), | ||
| 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"), | ||
| values: z.array(z.string()).optional().describe("Values to filter by (not needed for set/notSet operators)"), | ||
| })).optional().describe("Filters to apply"), | ||
| segments: z.array(z.string()).optional().describe("Pre-defined filter segments"), | ||
| order: z.record(z.enum(["asc", "desc"])).optional().describe("Sort order (e.g. {\"orders.revenue\": \"desc\"})"), | ||
| limit: z.number().optional().describe("Maximum rows to return (default: 250, max: 5000)"), | ||
| 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"), | ||
| }); | ||
| const describeFieldSchema = z.object({ | ||
| field: z.string().describe("Fully qualified field name (e.g. \"orders.revenue\")"), | ||
| }); | ||
| export function createTools(client) { | ||
| const exploreSchema = { | ||
| name: "explore_schema", | ||
| description: "Discover available data sources (views), their measures, dimensions, and segments. " + | ||
| "No arguments returns a summary of all sources. Use 'name' to get full field listings for one source. " + | ||
| "Use 'search' to find fields by keyword across all sources.", | ||
| schema: exploreSchemaSchema, | ||
| execute: async (args) => { | ||
| const meta = await client.explore({ viewsOnly: false }); | ||
| const cubes = meta.cubes; | ||
| if (args.search) { | ||
| const keyword = args.search.toLowerCase(); | ||
| const results = []; | ||
| const MAX_SEARCH = 50; | ||
| for (const cube of cubes) { | ||
| if (results.length >= MAX_SEARCH) | ||
| break; | ||
| const sourceType = cube.type === "view" ? "view" : "cube"; | ||
| for (const m of cube.measures) { | ||
| if (results.length >= MAX_SEARCH) | ||
| break; | ||
| if (m.name.toLowerCase().includes(keyword) || | ||
| m.description?.toLowerCase().includes(keyword) || | ||
| m.title?.toLowerCase().includes(keyword)) { | ||
| results.push({ source: cube.name, sourceType, field: m.name, kind: "measure", type: m.type, description: m.description }); | ||
| } | ||
| } | ||
| for (const d of cube.dimensions) { | ||
| if (results.length >= MAX_SEARCH) | ||
| break; | ||
| if (d.name.toLowerCase().includes(keyword) || | ||
| d.description?.toLowerCase().includes(keyword) || | ||
| d.title?.toLowerCase().includes(keyword)) { | ||
| results.push({ source: cube.name, sourceType, field: d.name, kind: "dimension", type: d.type, description: d.description }); | ||
| } | ||
| } | ||
| for (const s of cube.segments) { | ||
| if (results.length >= MAX_SEARCH) | ||
| break; | ||
| if (s.name.toLowerCase().includes(keyword) || | ||
| s.description?.toLowerCase().includes(keyword) || | ||
| s.title?.toLowerCase().includes(keyword)) { | ||
| results.push({ source: cube.name, sourceType, field: s.name, kind: "segment", type: "segment", description: s.description }); | ||
| } | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| if (args.name) { | ||
| const cube = cubes.find((c) => c.name === args.name); | ||
| if (!cube) { | ||
| return { error: `Source '${args.name}' not found. Available sources: ${cubes.map((c) => c.name).join(", ")}` }; | ||
| } | ||
| const dims = cube.dimensions.filter((d) => d.type !== "time"); | ||
| const timeDims = cube.dimensions.filter((d) => d.type === "time"); | ||
| return { | ||
| name: cube.name, | ||
| type: cube.type, | ||
| description: cube.description, | ||
| measures: cube.measures, | ||
| dimensions: dims, | ||
| timeDimensions: timeDims, | ||
| segments: cube.segments, | ||
| }; | ||
| } | ||
| return cubes.map((c) => ({ | ||
| name: c.name, | ||
| type: c.type, | ||
| description: c.description, | ||
| measures: c.measures.length, | ||
| dimensions: c.dimensions.filter((d) => d.type !== "time").length, | ||
| timeDimensions: c.dimensions.filter((d) => d.type === "time").length, | ||
| segments: c.segments.length, | ||
| })); | ||
| }, | ||
| }; | ||
| const query = { | ||
| name: "query", | ||
| description: "Query the semantic layer with measures, dimensions, filters, and time dimensions. " + | ||
| "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.", | ||
| 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 = args.order; | ||
| 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}.`; | ||
| } | ||
| return response; | ||
| }, | ||
| }; | ||
| const sqlQuery = { | ||
| name: "sql_query", | ||
| description: "Execute raw SQL against the semantic layer. Only use when the query tool cannot express what you need " + | ||
| "(CTEs, UNIONs, custom arithmetic, CASE expressions). Use MEASURE() for aggregations.", | ||
| schema: sqlQuerySchema, | ||
| execute: async (args) => { | ||
| try { | ||
| const result = await client.sql(args.sql); | ||
| 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; | ||
| const rows = stripPrefixes(capped); | ||
| const response = { | ||
| data_completeness: isPartial ? "partial" : "complete", | ||
| rows_shown: rows.length, | ||
| results: rows, | ||
| }; | ||
| if (isPartial) { | ||
| response.warning = `Partial results (${data.length} total). Do not sum or average these rows. Add LIMIT/OFFSET to your SQL to page through results.`; | ||
| } | ||
| return response; | ||
| } | ||
| catch (err) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| const hints = generateSqlErrorHints(message, args.sql); | ||
| return { error: message, hints }; | ||
| } | ||
| }, | ||
| }; | ||
| const describeField = { | ||
| name: "describe_field", | ||
| 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\").", | ||
| schema: describeFieldSchema, | ||
| execute: async (args) => { | ||
| const dotIndex = args.field.indexOf("."); | ||
| if (dotIndex === -1) { | ||
| return { error: "Field must be fully qualified (e.g. \"orders.revenue\")" }; | ||
| } | ||
| const sourceName = args.field.substring(0, dotIndex); | ||
| const fieldName = args.field; | ||
| const meta = await client.explore({ viewsOnly: false }); | ||
| const cube = meta.cubes.find((c) => c.name === sourceName); | ||
| if (!cube) { | ||
| return { error: `Source '${sourceName}' not found. Available sources: ${meta.cubes.map((c) => c.name).join(", ")}` }; | ||
| } | ||
| const sourceType = cube.type === "view" ? "view" : "cube"; | ||
| const measure = cube.measures.find((m) => m.name === fieldName); | ||
| if (measure) { | ||
| return { | ||
| name: measure.name, | ||
| kind: "measure", | ||
| type: measure.type, | ||
| description: measure.description, | ||
| ...(measure.format && { format: measure.format }), | ||
| ...(measure.meta && Object.keys(measure.meta).length > 0 && { meta: measure.meta }), | ||
| source: cube.name, | ||
| sourceType, | ||
| }; | ||
| } | ||
| const dimension = cube.dimensions.find((d) => d.name === fieldName); | ||
| if (dimension) { | ||
| return { | ||
| name: dimension.name, | ||
| kind: "dimension", | ||
| type: dimension.type, | ||
| description: dimension.description, | ||
| ...(dimension.format && { format: dimension.format }), | ||
| ...(dimension.meta && Object.keys(dimension.meta).length > 0 && { meta: dimension.meta }), | ||
| source: cube.name, | ||
| sourceType, | ||
| }; | ||
| } | ||
| const segment = cube.segments.find((s) => s.name === fieldName); | ||
| if (segment) { | ||
| return { name: segment.name, kind: "segment", type: "segment", description: segment.description, source: cube.name, sourceType }; | ||
| } | ||
| return { error: `Field '${fieldName}' not found in source '${sourceName}'` }; | ||
| }, | ||
| }; | ||
| return [exploreSchema, query, sqlQuery, describeField]; | ||
| } |
| import type { z } from "zod"; | ||
| import type { createClient } from "../client.js"; | ||
| export type BonnardClient = ReturnType<typeof createClient>; | ||
| export interface BonnardTool<T = any> { | ||
| name: string; | ||
| description: string; | ||
| schema: z.ZodType<T>; | ||
| execute: (args: T) => Promise<unknown>; | ||
| } |
| export {}; |
| import { type CoreTool } from "ai"; | ||
| import type { BonnardClient } from "./types.js"; | ||
| /** | ||
| * Create Bonnard tools for the Vercel AI SDK. | ||
| * | ||
| * Returns a `Record<string, CoreTool>` so tools can be spread directly: | ||
| * ```ts | ||
| * import { createTools } from "@bonnard/sdk/ai/vercel" | ||
| * const tools = createTools(bonnardClient) | ||
| * const result = await generateText({ model, tools: { ...tools, ...myOtherTools } }) | ||
| * ``` | ||
| */ | ||
| export declare function createTools(client: BonnardClient): Record<string, CoreTool>; |
| import { tool } from "ai"; | ||
| import { createTools as createBonnardTools } from "./tools.js"; | ||
| /** | ||
| * Create Bonnard tools for the Vercel AI SDK. | ||
| * | ||
| * Returns a `Record<string, CoreTool>` so tools can be spread directly: | ||
| * ```ts | ||
| * import { createTools } from "@bonnard/sdk/ai/vercel" | ||
| * const tools = createTools(bonnardClient) | ||
| * const result = await generateText({ model, tools: { ...tools, ...myOtherTools } }) | ||
| * ``` | ||
| */ | ||
| export function createTools(client) { | ||
| const bonnardTools = createBonnardTools(client); | ||
| const tools = {}; | ||
| for (const t of bonnardTools) { | ||
| tools[t.name] = tool({ | ||
| description: t.description, | ||
| parameters: t.schema, | ||
| execute: t.execute, | ||
| }); | ||
| } | ||
| return tools; | ||
| } |
@@ -1,2 +0,2 @@ | ||
| "use strict";var Bonnard=(()=>{var y=Object.defineProperty;var f=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var T=Object.prototype.hasOwnProperty;var b=(e,r)=>{for(var o in r)y(e,o,{get:r[o],enumerable:!0})},R=(e,r,o,i)=>{if(r&&typeof r=="object"||typeof r=="function")for(let a of w(r))!T.call(e,a)&&a!==o&&y(e,a,{get:()=>r[a],enumerable:!(i=f(r,a))||i.enumerable});return e};var g=e=>R(y({},"__esModule",{value:!0}),e);var k={};b(k,{createClient:()=>p,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,i])=>[o,i])),e.limit&&(r.limit=e.limit),r}function h(e){try{let r=e.split(".");if(r.length!==3)return 0;let o=r[1].replace(/-/g,"+").replace(/_/g,"/"),i=JSON.parse(atob(o));return typeof i.exp=="number"?i.exp*1e3:0}catch{return 0}}var Q=6e4;function p(e){let r=e.baseUrl||"https://app.bonnard.dev",o=null,i=0;async function a(){if(e.apiKey)return e.apiKey;if(e.fetchToken){let n=Date.now();if(o&&i-Q>n)return o;let t=await e.fetchToken();return o=t,i=h(t),t}throw new Error("BonnardConfig requires either apiKey or fetchToken")}async function l(n,t){let s=await a(),u=await fetch(`${r}${n}`,{method:"POST",headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:JSON.stringify(t)});if(!u.ok){let d=await u.json().catch(()=>({error:u.statusText}));throw new Error(d.error||"Query failed")}return u.json()}async function m(n){let t=await a(),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 m("/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 m(`/api/docs${s?`?${s}`:""}`)}}}return g(k);})(); | ||
| "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);})(); | ||
| //# 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\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\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 and refresh\n if (config.fetchToken) {\n const now = Date.now();\n if (cachedToken && cachedExpiry - REFRESH_BUFFER_MS > now) {\n return cachedToken;\n }\n\n const token = await config.fetchToken();\n cachedToken = token;\n cachedExpiry = parseJwtExpiry(token);\n return token;\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": "2bAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,kBAAAE,EAAA,gBAAAC,ICUO,SAASC,EAAYC,EAAgD,CAC1E,IAAMC,EAAqC,CAAC,EAE5C,OAAID,EAAQ,WACVC,EAAU,SAAWD,EAAQ,UAG3BA,EAAQ,aACVC,EAAU,WAAaD,EAAQ,YAG7BA,EAAQ,UACVC,EAAU,QAAUD,EAAQ,QAAQ,IAAIE,IAAM,CAC5C,OAAQA,EAAE,UACV,SAAUA,EAAE,SACZ,OAAQA,EAAE,MACZ,EAAE,GAGAF,EAAQ,gBACVC,EAAU,eAAiB,CAAC,CAC1B,UAAWD,EAAQ,cAAc,UACjC,YAAaA,EAAQ,cAAc,YACnC,UAAWA,EAAQ,cAAc,SACnC,CAAC,GAGCA,EAAQ,UACVC,EAAU,MAAQ,OAAO,QAAQD,EAAQ,OAAO,EAAE,IAAI,CAAC,CAACG,EAAKC,CAAG,IAAM,CAACD,EAAKC,CAAG,CAAC,GAG9EJ,EAAQ,QACVC,EAAU,MAAQD,EAAQ,OAGrBC,CACT,CCxBA,SAASI,EAAeC,EAAuB,CAC7C,GAAI,CACF,IAAMC,EAAQD,EAAM,MAAM,GAAG,EAC7B,GAAIC,EAAM,SAAW,EAAG,MAAO,GAE/B,IAAMC,EAAUD,EAAM,CAAC,EAAG,QAAQ,KAAM,GAAG,EAAE,QAAQ,KAAM,GAAG,EACxDE,EAAO,KAAK,MAAM,KAAKD,CAAO,CAAC,EACrC,OAAO,OAAOC,EAAK,KAAQ,SAAWA,EAAK,IAAM,IAAO,CAC1D,MAAQ,CACN,MAAO,EACT,CACF,CAEA,IAAMC,EAAoB,IAEnB,SAASC,EAAaC,EAAuB,CAClD,IAAMC,EAAUD,EAAO,SAAW,0BAG9BE,EAA6B,KAC7BC,EAAe,EAEnB,eAAeC,GAA4B,CAEzC,GAAIJ,EAAO,OACT,OAAOA,EAAO,OAIhB,GAAIA,EAAO,WAAY,CACrB,IAAMK,EAAM,KAAK,IAAI,EACrB,GAAIH,GAAeC,EAAeL,EAAoBO,EACpD,OAAOH,EAGT,IAAMR,EAAQ,MAAMM,EAAO,WAAW,EACtC,OAAAE,EAAcR,EACdS,EAAeV,EAAeC,CAAK,EAC5BA,CACT,CAEA,MAAM,IAAI,MAAM,oDAAoD,CACtE,CAEA,eAAeY,EAAWC,EAAkBC,EAA2B,CACrE,IAAMd,EAAQ,MAAMU,EAAS,EACvBK,EAAM,MAAM,MAAM,GAAGR,CAAO,GAAGM,CAAQ,GAAI,CAC/C,OAAQ,OACR,QAAS,CACP,cAAiB,UAAUb,CAAK,GAChC,eAAgB,kBAClB,EACA,KAAM,KAAK,UAAUc,CAAI,CAC3B,CAAC,EAED,GAAI,CAACC,EAAI,GAAI,CACX,IAAMC,EAAQ,MAAMD,EAAI,KAAK,EAAE,MAAM,KAAO,CAAE,MAAOA,EAAI,UAAW,EAAE,EACtE,MAAM,IAAI,MAAMC,EAAM,OAAS,cAAc,CAC/C,CAEA,OAAOD,EAAI,KAAK,CAClB,CAEA,eAAeE,EAAcJ,EAA8B,CACzD,IAAMb,EAAQ,MAAMU,EAAS,EACvBK,EAAM,MAAM,MAAM,GAAGR,CAAO,GAAGM,CAAQ,GAAI,CAC/C,OAAQ,MACR,QAAS,CACP,cAAiB,UAAUb,CAAK,EAClC,CACF,CAAC,EAED,GAAI,CAACe,EAAI,GAAI,CACX,IAAMC,EAAQ,MAAMD,EAAI,KAAK,EAAE,MAAM,KAAO,CAAE,MAAOA,EAAI,UAAW,EAAE,EACtE,MAAM,IAAI,MAAMC,EAAM,OAAS,gBAAgB,CACjD,CAEA,OAAOD,EAAI,KAAK,CAClB,CAEA,MAAO,CAKL,MAAM,MAAmCG,EAAgD,CACvF,IAAMC,EAAYC,EAAYF,CAAO,EAE/BG,EAAS,MAAMT,EACnB,kBACA,CAAE,MAAOO,CAAU,CACrB,EAEA,MAAO,CAAE,KAAME,EAAO,KAAM,WAAYA,EAAO,UAAW,CAC5D,EAMA,MAAM,SAAsCF,EAA+C,CACzF,IAAME,EAAS,MAAMT,EACnB,kBACA,CAAE,MAAOO,CAAU,CACrB,EAEA,MAAO,CAAE,KAAME,EAAO,KAAM,WAAYA,EAAO,UAAW,CAC5D,EAKA,MAAM,IAAiCC,EAAsC,CAC3E,OAAOV,EAAsB,kBAAmB,CAAE,IAAKU,CAAM,CAAC,CAChE,EAMA,MAAM,QAAQJ,EAAgD,CAC5D,IAAMK,EAAO,MAAMN,EAA4C,gBAAgB,EAG/E,OAFkBC,GAAS,WAAa,GAG/B,CAAE,MAAOK,EAAK,MAAM,OAAOC,GAAKA,EAAE,OAAS,MAAM,CAAE,EAGrDD,CACT,EAQA,MAAM,KAAKL,EAAuE,CAChF,IAAMO,EAAS,IAAI,gBACfP,GAAS,MAAOO,EAAO,IAAI,QAASP,EAAQ,KAAK,EAC5CA,GAAS,UAAUO,EAAO,IAAI,WAAYP,EAAQ,QAAQ,EACnE,IAAMQ,EAAKD,EAAO,SAAS,EAC3B,OAAOR,EAAW,YAAYS,EAAK,IAAIA,CAAE,GAAK,EAAE,EAAE,CACpD,CACF,CACF", | ||
| "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", "getToken", "now", "request", "endpoint", "body", "res", "error", "requestGet", "options", "cubeQuery", "toCubeQuery", "result", "query", "meta", "c", "params", "qs"] | ||
| "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"] | ||
| } |
+7
-0
@@ -5,2 +5,9 @@ /** | ||
| import type { BonnardConfig, QueryOptions, QueryResult, SqlResult, CubeQuery, ExploreMeta, ExploreOptions, DocsOptions, DocsTopicListResult, DocsTopicResult } from './types.js'; | ||
| /** | ||
| * Create a Bonnard client. | ||
| * | ||
| * When `fetchToken` is provided, the returned token is cached automatically | ||
| * and refreshed 60 seconds before its JWT `exp` claim. Concurrent calls are | ||
| * deduplicated so your callback is never invoked more than once at a time. | ||
| */ | ||
| export declare function createClient(config: BonnardConfig): { | ||
@@ -7,0 +14,0 @@ /** |
+22
-5
@@ -24,2 +24,9 @@ /** | ||
| const REFRESH_BUFFER_MS = 60_000; // refresh 60s before expiry | ||
| /** | ||
| * Create a Bonnard client. | ||
| * | ||
| * When `fetchToken` is provided, the returned token is cached automatically | ||
| * and refreshed 60 seconds before its JWT `exp` claim. Concurrent calls are | ||
| * deduplicated so your callback is never invoked more than once at a time. | ||
| */ | ||
| export function createClient(config) { | ||
@@ -30,2 +37,3 @@ const baseUrl = config.baseUrl || 'https://app.bonnard.dev'; | ||
| let cachedExpiry = 0; | ||
| let pendingFetch = null; | ||
| async function getToken() { | ||
@@ -36,3 +44,3 @@ // Static API key — return directly | ||
| } | ||
| // Token callback — cache and refresh | ||
| // Token callback — cache, refresh, and deduplicate | ||
| if (config.fetchToken) { | ||
@@ -43,6 +51,15 @@ const now = Date.now(); | ||
| } | ||
| const token = await config.fetchToken(); | ||
| cachedToken = token; | ||
| cachedExpiry = parseJwtExpiry(token); | ||
| return token; | ||
| // Deduplicate concurrent calls — share a single in-flight promise | ||
| if (pendingFetch) | ||
| return pendingFetch; | ||
| pendingFetch = config.fetchToken() | ||
| .then((token) => { | ||
| cachedToken = token; | ||
| cachedExpiry = parseJwtExpiry(token); | ||
| return token; | ||
| }) | ||
| .finally(() => { | ||
| pendingFetch = null; | ||
| }); | ||
| return pendingFetch; | ||
| } | ||
@@ -49,0 +66,0 @@ throw new Error('BonnardConfig requires either apiKey or fetchToken'); |
+14
-0
@@ -5,4 +5,16 @@ /** | ||
| export interface BonnardConfig { | ||
| /** Publishable API key (`bon_pk_…`). Mutually exclusive with `fetchToken`. */ | ||
| apiKey?: string; | ||
| /** | ||
| * Callback that returns a short-lived JWT from your server. | ||
| * | ||
| * The SDK **automatically caches** the token and re-calls this function | ||
| * 60 seconds before the JWT's `exp` claim. Concurrent calls while a fetch | ||
| * is in-flight are deduplicated — you do **not** need to cache the token | ||
| * yourself. | ||
| * | ||
| * Mutually exclusive with `apiKey`. | ||
| */ | ||
| fetchToken?: () => Promise<string>; | ||
| /** API base URL. Defaults to `https://app.bonnard.dev`. */ | ||
| baseUrl?: string; | ||
@@ -84,2 +96,4 @@ } | ||
| type: string; | ||
| format?: string; | ||
| meta?: Record<string, unknown>; | ||
| } | ||
@@ -86,0 +100,0 @@ export interface CubeSegmentMeta { |
+34
-3
| { | ||
| "name": "@bonnard/sdk", | ||
| "version": "0.4.2", | ||
| "version": "0.4.3", | ||
| "description": "Bonnard SDK - query your semantic layer from any JavaScript or TypeScript app", | ||
@@ -13,2 +13,14 @@ "type": "module", | ||
| }, | ||
| "./ai": { | ||
| "import": "./dist/ai/index.js", | ||
| "types": "./dist/ai/index.d.ts" | ||
| }, | ||
| "./ai/vercel": { | ||
| "import": "./dist/ai/vercel.js", | ||
| "types": "./dist/ai/vercel.d.ts" | ||
| }, | ||
| "./ai/langchain": { | ||
| "import": "./dist/ai/langchain.js", | ||
| "types": "./dist/ai/langchain.d.ts" | ||
| }, | ||
| "./browser": { | ||
@@ -28,5 +40,24 @@ "default": "./dist/bonnard.iife.js" | ||
| "dependencies": {}, | ||
| "peerDependencies": { | ||
| "zod": "^3.0.0", | ||
| "ai": "^4.0.0", | ||
| "@langchain/core": "^1.0.0" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "zod": { | ||
| "optional": true | ||
| }, | ||
| "ai": { | ||
| "optional": true | ||
| }, | ||
| "@langchain/core": { | ||
| "optional": true | ||
| } | ||
| }, | ||
| "devDependencies": { | ||
| "esbuild": "^0.24.0", | ||
| "typescript": "^5.3.3" | ||
| "esbuild": "^0.27.3", | ||
| "typescript": "^5.3.3", | ||
| "zod": "^3.24.0", | ||
| "ai": "^4.0.0", | ||
| "@langchain/core": "^1.0.0" | ||
| }, | ||
@@ -33,0 +64,0 @@ "keywords": [ |
50310
75.74%25
66.67%782
124.71%3
Infinity%5
150%