yamchart 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CLAUDE.md +51 -0
  2. package/dist/{advisor-5BTRAICH.js → advisor-57BOMUUF.js} +11 -10
  3. package/dist/{advisor-5BTRAICH.js.map → advisor-57BOMUUF.js.map} +1 -1
  4. package/dist/{chunk-5VA43CTW.js → chunk-E2UZXDF6.js} +37 -42
  5. package/dist/chunk-E2UZXDF6.js.map +1 -0
  6. package/dist/{chunk-SSUVEADJ.js → chunk-JYQKDWLG.js} +76 -14
  7. package/dist/chunk-JYQKDWLG.js.map +1 -0
  8. package/dist/{chunk-565OEBW7.js → chunk-OTAUP5RC.js} +3 -3
  9. package/dist/{chunk-F6QAWPYU.js → chunk-TZKNU5TD.js} +2 -2
  10. package/dist/chunk-TZKNU5TD.js.map +1 -0
  11. package/dist/chunk-UND73EOB.js +449 -0
  12. package/dist/chunk-UND73EOB.js.map +1 -0
  13. package/dist/{chunk-UDRJFEJU.js → chunk-X6LQGWUX.js} +349 -176
  14. package/dist/chunk-X6LQGWUX.js.map +1 -0
  15. package/dist/{connection-utils-AGSQ5HNN.js → connection-utils-FTSZU2VF.js} +5 -4
  16. package/dist/{describe-AA4UT4U6.js → describe-TGIOBNJB.js} +5 -4
  17. package/dist/{describe-AA4UT4U6.js.map → describe-TGIOBNJB.js.map} +1 -1
  18. package/dist/{dev-EUWIRL4N.js → dev-MRZ76V74.js} +485 -44
  19. package/dist/dev-MRZ76V74.js.map +1 -0
  20. package/dist/{dist-LZNDQDH3.js → dist-PVHFUYP2.js} +23 -3
  21. package/dist/{dist-YTGUIBKG.js → dist-WDTDQDTX.js} +72 -1
  22. package/dist/dist-WDTDQDTX.js.map +1 -0
  23. package/dist/index.js +136 -18
  24. package/dist/index.js.map +1 -1
  25. package/dist/{init-CI4VARQG.js → init-UYQE5DPU.js} +2 -2
  26. package/dist/{init-CI4VARQG.js.map → init-UYQE5DPU.js.map} +1 -1
  27. package/dist/public/assets/EventManagement-BlxJ2TFw.js +18 -0
  28. package/dist/public/assets/{LoginPage-BwMmpq8e.js → LoginPage-BT8ikmPn.js} +1 -1
  29. package/dist/public/assets/PublicViewer-B4uFxgbt.js +1 -0
  30. package/dist/public/assets/{SetupWizard-DU8R2dPp.js → SetupWizard-njrOhCzw.js} +1 -1
  31. package/dist/public/assets/ShareManagement-Bg16oJhW.js +1 -0
  32. package/dist/public/assets/UserManagement-tiCIT4UY.js +1 -0
  33. package/dist/public/assets/index-B41yj3io.js +187 -0
  34. package/dist/public/assets/{index-C0hWblBI.css → index-BZ25r23j.css} +1 -1
  35. package/dist/public/assets/{index.es-YeJujQ5o.js → index.es-BuktD_R2.js} +1 -1
  36. package/dist/public/assets/{jspdf.es.min-BzZgn3ka.js → jspdf.es.min-DFRl2hZQ.js} +3 -3
  37. package/dist/public/index.html +2 -2
  38. package/dist/{query-THDVBBVZ.js → query-JRMMNXX6.js} +5 -4
  39. package/dist/{query-THDVBBVZ.js.map → query-JRMMNXX6.js.map} +1 -1
  40. package/dist/{sample-6WPQS7PA.js → sample-VGIY4U4J.js} +5 -4
  41. package/dist/{sample-6WPQS7PA.js.map → sample-VGIY4U4J.js.map} +1 -1
  42. package/dist/{search-657DXBRS.js → search-QSYNG4SR.js} +5 -4
  43. package/dist/{search-657DXBRS.js.map → search-QSYNG4SR.js.map} +1 -1
  44. package/dist/semantic-RAP3S5PQ.js +39 -0
  45. package/dist/semantic-RAP3S5PQ.js.map +1 -0
  46. package/dist/{sync-warehouse-4KBV5S3L.js → sync-warehouse-XHTBZH25.js} +5 -4
  47. package/dist/{sync-warehouse-4KBV5S3L.js.map → sync-warehouse-XHTBZH25.js.map} +1 -1
  48. package/dist/{tables-KLDBUUSE.js → tables-UOO342TA.js} +5 -4
  49. package/dist/{tables-KLDBUUSE.js.map → tables-UOO342TA.js.map} +1 -1
  50. package/dist/templates/default/.claude/skills/databricks/SKILL.md +76 -0
  51. package/dist/templates/default/.claude/skills/dbt-advisor/SKILL.md +107 -0
  52. package/dist/templates/default/.claude/skills/duckdb/SKILL.md +67 -0
  53. package/dist/templates/default/.claude/skills/mysql/SKILL.md +69 -0
  54. package/dist/templates/default/.claude/skills/postgres/SKILL.md +69 -0
  55. package/dist/templates/default/.claude/skills/snowflake/SKILL.md +64 -0
  56. package/dist/templates/default/CLAUDE.md +8 -1
  57. package/dist/templates/default/docs/yamchart-reference.md +222 -1
  58. package/dist/{test-JSAWS5ZP.js → test-TXRZWNXK.js} +5 -4
  59. package/dist/{test-JSAWS5ZP.js.map → test-TXRZWNXK.js.map} +1 -1
  60. package/dist/update-WMATDZTO.js +220 -0
  61. package/dist/update-WMATDZTO.js.map +1 -0
  62. package/package.json +8 -6
  63. package/dist/chunk-5VA43CTW.js.map +0 -1
  64. package/dist/chunk-F6QAWPYU.js.map +0 -1
  65. package/dist/chunk-SSUVEADJ.js.map +0 -1
  66. package/dist/chunk-UDRJFEJU.js.map +0 -1
  67. package/dist/dev-EUWIRL4N.js.map +0 -1
  68. package/dist/dist-YTGUIBKG.js.map +0 -1
  69. package/dist/public/assets/PublicViewer-vIDojjIR.js +0 -1
  70. package/dist/public/assets/ShareManagement-7iS6lM2T.js +0 -1
  71. package/dist/public/assets/UserManagement-By7YRZrF.js +0 -1
  72. package/dist/public/assets/index-CXx1PiRF.js +0 -174
  73. package/dist/update-HCR6MYJX.js +0 -88
  74. package/dist/update-HCR6MYJX.js.map +0 -1
  75. /package/dist/{chunk-565OEBW7.js.map → chunk-OTAUP5RC.js.map} +0 -0
  76. /package/dist/{connection-utils-AGSQ5HNN.js.map → connection-utils-FTSZU2VF.js.map} +0 -0
  77. /package/dist/{dist-LZNDQDH3.js.map → dist-PVHFUYP2.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadEnvFile,
3
3
  validateProject
4
- } from "./chunk-5VA43CTW.js";
4
+ } from "./chunk-E2UZXDF6.js";
5
5
  import {
6
6
  box,
7
7
  detail,
@@ -16,9 +16,19 @@ import {
16
16
  ChartSchema,
17
17
  ConnectionSchema,
18
18
  DashboardSchema,
19
+ EventSchema,
20
+ EventsFileSchema,
19
21
  ProjectSchema,
20
- ScheduleSchema
21
- } from "./chunk-UDRJFEJU.js";
22
+ ScheduleSchema,
23
+ SemanticQuerySchema
24
+ } from "./chunk-X6LQGWUX.js";
25
+ import {
26
+ AuthDatabase,
27
+ generateSessionToken,
28
+ hashPassword,
29
+ parseTtl,
30
+ verifyPassword
31
+ } from "./chunk-4P5UHWYK.js";
22
32
  import {
23
33
  DuckDBConnector,
24
34
  MySQLConnector,
@@ -40,14 +50,13 @@ import {
40
50
  resolveMySQLAuth,
41
51
  resolvePostgresAuth,
42
52
  resolveSnowflakeAuth
43
- } from "./chunk-SSUVEADJ.js";
53
+ } from "./chunk-JYQKDWLG.js";
44
54
  import {
45
- AuthDatabase,
46
- generateSessionToken,
47
- hashPassword,
48
- parseTtl,
49
- verifyPassword
50
- } from "./chunk-4P5UHWYK.js";
55
+ SemanticModelBuilder,
56
+ SemanticQueryCompiler,
57
+ SemanticValidationError,
58
+ validateSemanticQuery
59
+ } from "./chunk-UND73EOB.js";
51
60
  import "./chunk-DGUM43GV.js";
52
61
 
53
62
  // src/commands/dev.ts
@@ -76,6 +85,8 @@ var ConfigLoader = class {
76
85
  dashboardFolders = /* @__PURE__ */ new Map();
77
86
  dashboardFilePaths = /* @__PURE__ */ new Map();
78
87
  schedules = /* @__PURE__ */ new Map();
88
+ events = [];
89
+ validationErrors = /* @__PURE__ */ new Map();
79
90
  watcher = null;
80
91
  onChangeCallbacks = [];
81
92
  constructor(projectDir) {
@@ -91,12 +102,15 @@ var ConfigLoader = class {
91
102
  this.dashboardFolders.clear();
92
103
  this.dashboardFilePaths.clear();
93
104
  this.schedules.clear();
105
+ this.events = [];
106
+ this.validationErrors.clear();
94
107
  await this.loadProject();
95
108
  await this.loadConnections();
96
109
  await this.loadCharts();
97
110
  await this.loadModels();
98
111
  await this.loadDashboards();
99
112
  await this.loadSchedules();
113
+ await this.loadEvents();
100
114
  }
101
115
  async loadProject() {
102
116
  const projectPath = join(this.projectDir, "yamchart.yaml");
@@ -165,7 +179,16 @@ var ConfigLoader = class {
165
179
  const relDir = relative(baseDir, dirname(fullPath)).replace(/\\/g, "/");
166
180
  this.chartFolders.set(name, relDir || "");
167
181
  } else {
168
- console.warn(`Invalid chart file ${entry.name}: ${result.error.message}`);
182
+ const relPath = relative(baseDir, fullPath);
183
+ const name = parsed && typeof parsed === "object" && "name" in parsed && typeof parsed.name === "string" ? parsed.name : void 0;
184
+ const errorMsg = result.error.errors.map((e) => {
185
+ const path = e.path.length > 0 ? e.path.join(".") : "(root)";
186
+ return `${path}: ${e.message}`;
187
+ }).join("; ");
188
+ console.warn(`Invalid chart file ${relPath}: ${errorMsg}`);
189
+ if (name) {
190
+ this.validationErrors.set(name, { file: relPath, message: errorMsg });
191
+ }
169
192
  }
170
193
  }
171
194
  }
@@ -230,7 +253,16 @@ var ConfigLoader = class {
230
253
  const relDir = relative(baseDir, dirname(fullPath)).replace(/\\/g, "/");
231
254
  this.dashboardFolders.set(name, relDir || "");
232
255
  } else {
233
- console.warn(`Invalid dashboard file ${entry.name}: ${result.error.message}`);
256
+ const relPath = relative(baseDir, fullPath);
257
+ const name = parsed && typeof parsed === "object" && "name" in parsed && typeof parsed.name === "string" ? parsed.name : void 0;
258
+ const errorMsg = result.error.errors.map((e) => {
259
+ const path = e.path.length > 0 ? e.path.join(".") : "(root)";
260
+ return `${path}: ${e.message}`;
261
+ }).join("; ");
262
+ console.warn(`Invalid dashboard file ${relPath}: ${errorMsg}`);
263
+ if (name) {
264
+ this.validationErrors.set(name, { file: relPath, message: errorMsg });
265
+ }
234
266
  }
235
267
  }
236
268
  }
@@ -257,6 +289,22 @@ var ConfigLoader = class {
257
289
  }
258
290
  }
259
291
  }
292
+ async loadEvents() {
293
+ const eventsPath = join(this.projectDir, "events.yaml");
294
+ try {
295
+ await access(eventsPath);
296
+ } catch {
297
+ return;
298
+ }
299
+ const content = await readFile(eventsPath, "utf-8");
300
+ const parsed = parseYaml(content);
301
+ const result = EventsFileSchema.safeParse(parsed);
302
+ if (result.success) {
303
+ this.events = result.data.events;
304
+ } else {
305
+ console.warn(`Invalid events.yaml: ${result.error.message}`);
306
+ }
307
+ }
260
308
  startWatching() {
261
309
  if (this.watcher)
262
310
  return;
@@ -266,7 +314,8 @@ var ConfigLoader = class {
266
314
  join(this.projectDir, "charts"),
267
315
  join(this.projectDir, "models"),
268
316
  join(this.projectDir, "dashboards"),
269
- join(this.projectDir, "schedules")
317
+ join(this.projectDir, "schedules"),
318
+ join(this.projectDir, "events.yaml")
270
319
  ];
271
320
  this.watcher = watch(watchPaths, {
272
321
  ignored: /(^|[\/\\])\../,
@@ -288,6 +337,17 @@ var ConfigLoader = class {
288
337
  console.log(`File removed: ${path}`);
289
338
  await this.reload();
290
339
  });
340
+ this.watcher.on("error", (err) => {
341
+ const code = err.code;
342
+ if (code === "EMFILE") {
343
+ console.warn(`Warning: Too many open files for file watching. The server will continue running but won't auto-reload on file changes.
344
+ Fix: run "ulimit -n 10240" before starting yamchart dev, or add it to your shell profile.`);
345
+ this.watcher?.close();
346
+ this.watcher = null;
347
+ } else {
348
+ console.error("File watcher error:", err);
349
+ }
350
+ });
291
351
  }
292
352
  async reload() {
293
353
  try {
@@ -364,6 +424,18 @@ var ConfigLoader = class {
364
424
  getSchedules() {
365
425
  return Array.from(this.schedules.values());
366
426
  }
427
+ getEvents() {
428
+ return this.events;
429
+ }
430
+ getProjectDir() {
431
+ return this.projectDir;
432
+ }
433
+ getValidationError(name) {
434
+ return this.validationErrors.get(name);
435
+ }
436
+ getValidationErrors() {
437
+ return this.validationErrors;
438
+ }
367
439
  };
368
440
 
369
441
  // ../server/dist/services/cache.js
@@ -507,6 +579,24 @@ var QueryService = class {
507
579
  getCacheStats() {
508
580
  return this.cache.getStats();
509
581
  }
582
+ async executeRawSql(sql) {
583
+ return this.connector.execute(sql);
584
+ }
585
+ async executeModel(modelName, params, userContext, limit = 1e3, filters) {
586
+ const model = this.models.get(modelName);
587
+ if (!model) {
588
+ throw new Error(`Unknown model: ${modelName}`);
589
+ }
590
+ const context = createTemplateContext(params, this.refs, userContext);
591
+ let sql = renderTemplate(model.sql, context);
592
+ if (filters && Object.keys(filters).length > 0) {
593
+ const clauses = Object.entries(filters).map(([field, value]) => `${field} = '${value.replace(/'/g, "''")}'`);
594
+ sql = `SELECT * FROM (${sql}) _detail WHERE ${clauses.join(" AND ")} LIMIT ${limit}`;
595
+ } else {
596
+ sql = `SELECT * FROM (${sql}) _detail LIMIT ${limit}`;
597
+ }
598
+ return this.connector.execute(sql);
599
+ }
510
600
  async executeParameterOptions(modelName, valueField, labelField) {
511
601
  const model = this.models.get(modelName);
512
602
  if (!model) {
@@ -778,13 +868,30 @@ function buildComparisonResponse(input) {
778
868
  previousPeriodLabel: formatPeriodLabel(previousStart, previousEnd)
779
869
  };
780
870
  }
871
+ function chartNotFoundResponse(configLoader, name) {
872
+ const validationError = configLoader.getValidationError(name);
873
+ if (validationError) {
874
+ return {
875
+ status: 422,
876
+ body: {
877
+ error: `Invalid chart config "${name}" (${validationError.file}): ${validationError.message}`,
878
+ errorType: "validation_error"
879
+ }
880
+ };
881
+ }
882
+ return {
883
+ status: 404,
884
+ body: { error: `Chart not found: ${name}`, errorType: "not_found" }
885
+ };
886
+ }
781
887
  async function chartRoutes(fastify, options) {
782
888
  const { configLoader, queryService } = options;
783
889
  fastify.get("/api/charts/:name", async (request, reply) => {
784
890
  const { name } = request.params;
785
891
  const chart = configLoader.getChartByName(name);
786
892
  if (!chart) {
787
- return reply.status(404).send({ error: `Chart not found: ${name}`, errorType: "not_found" });
893
+ const resp = chartNotFoundResponse(configLoader, name);
894
+ return reply.status(resp.status).send(resp.body);
788
895
  }
789
896
  return {
790
897
  name: chart.name,
@@ -801,7 +908,8 @@ async function chartRoutes(fastify, options) {
801
908
  const includeConfig = request.query.includeConfig === "true";
802
909
  const chart = configLoader.getChartByName(name);
803
910
  if (!chart) {
804
- return reply.status(404).send({ error: `Chart not found: ${name}`, errorType: "not_found" });
911
+ const resp = chartNotFoundResponse(configLoader, name);
912
+ return reply.status(resp.status).send(resp.body);
805
913
  }
806
914
  try {
807
915
  const result = await queryService.executeChart(chart, params, getUserContext(request));
@@ -867,18 +975,70 @@ async function chartRoutes(fastify, options) {
867
975
  title: chart.title,
868
976
  description: chart.description,
869
977
  parameters: chart.parameters ?? [],
870
- chart: chart.chart
978
+ chart: chart.chart,
979
+ drillDown: chart.drillDown
871
980
  };
872
981
  }
873
982
  if (comparison) {
874
983
  response.comparison = comparison;
875
984
  }
985
+ let goalData = void 0;
986
+ const goals = chart.chart?.goals;
987
+ const modelGoal = goals?.find((g) => g.type === "model" && g.source?.model);
988
+ if (modelGoal?.source) {
989
+ try {
990
+ const goalResult = await queryService.executeModel(modelGoal.source.model, params, getUserContext(request));
991
+ goalData = { rows: goalResult.rows };
992
+ } catch {
993
+ }
994
+ }
995
+ if (goalData) {
996
+ response.goalData = goalData;
997
+ }
876
998
  return response;
877
999
  } catch (error2) {
878
1000
  const message = error2 instanceof Error ? error2.message : "Query execution failed";
879
1001
  return reply.status(500).send({ error: message, errorType: classifyError(error2) });
880
1002
  }
881
1003
  });
1004
+ fastify.post("/api/charts/:name/detail", async (request, reply) => {
1005
+ const { name } = request.params;
1006
+ const params = request.body ?? {};
1007
+ const chart = configLoader.getChartByName(name);
1008
+ if (!chart) {
1009
+ const resp = chartNotFoundResponse(configLoader, name);
1010
+ return reply.status(resp.status).send(resp.body);
1011
+ }
1012
+ const detailModel = chart.drillDown?.model;
1013
+ if (!detailModel) {
1014
+ return reply.status(400).send({
1015
+ error: "Chart has no detail model configured",
1016
+ errorType: "validation_error"
1017
+ });
1018
+ }
1019
+ try {
1020
+ const dd = chart.drillDown;
1021
+ const drillFields = dd.fields ? typeof dd.fields === "string" ? [dd.fields] : dd.fields : dd.field ? [dd.field] : [];
1022
+ const drillFilters = {};
1023
+ for (const f of drillFields) {
1024
+ if (params[f] != null && params[f] !== "") {
1025
+ drillFilters[f] = String(params[f]);
1026
+ }
1027
+ }
1028
+ const result = await queryService.executeModel(detailModel, params, getUserContext(request), 1e3, Object.keys(drillFilters).length > 0 ? drillFilters : void 0);
1029
+ return {
1030
+ columns: result.columns,
1031
+ rows: result.rows,
1032
+ meta: {
1033
+ rowCount: result.rowCount,
1034
+ durationMs: result.durationMs
1035
+ }
1036
+ };
1037
+ } catch (error2) {
1038
+ const message = error2 instanceof Error ? error2.message : "Detail query failed";
1039
+ return reply.status(500).send({ error: message, errorType: classifyError(error2) });
1040
+ }
1041
+ });
882
1042
  fastify.post("/api/charts/batch", async (request, reply) => {
883
1043
  const { charts, includeConfig } = request.body;
884
1044
  if (!charts || !Array.isArray(charts)) {
@@ -887,7 +1047,8 @@ async function chartRoutes(fastify, options) {
887
1047
  const results = await Promise.all(charts.map(async ({ name, params = {} }) => {
888
1048
  const chart = configLoader.getChartByName(name);
889
1049
  if (!chart) {
890
- return { name, error: `Chart not found: ${name}`, errorType: "not_found" };
1050
+ const resp = chartNotFoundResponse(configLoader, name);
1051
+ return { name, ...resp.body };
891
1052
  }
892
1053
  try {
893
1054
  const result = await queryService.executeChart(chart, params, getUserContext(request));
@@ -908,9 +1069,19 @@ async function chartRoutes(fastify, options) {
908
1069
  title: chart.title,
909
1070
  description: chart.description,
910
1071
  parameters: chart.parameters ?? [],
911
- chart: chart.chart
1072
+ chart: chart.chart,
1073
+ drillDown: chart.drillDown
912
1074
  };
913
1075
  }
1076
+ const goals = chart.chart?.goals;
1077
+ const modelGoal = goals?.find((g) => g.type === "model" && g.source?.model);
1078
+ if (modelGoal?.source) {
1079
+ try {
1080
+ const goalResult = await queryService.executeModel(modelGoal.source.model, params, getUserContext(request));
1081
+ response.goalData = { rows: goalResult.rows };
1082
+ } catch {
1083
+ }
1084
+ }
914
1085
  return response;
915
1086
  } catch (error2) {
916
1087
  const message = error2 instanceof Error ? error2.message : "Query failed";
@@ -923,7 +1094,8 @@ async function chartRoutes(fastify, options) {
923
1094
  const { name } = request.params;
924
1095
  const chart = configLoader.getChartByName(name);
925
1096
  if (!chart) {
926
- return reply.status(404).send({ error: `Chart not found: ${name}`, errorType: "not_found" });
1097
+ const resp = chartNotFoundResponse(configLoader, name);
1098
+ return reply.status(resp.status).send(resp.body);
927
1099
  }
928
1100
  queryService.invalidateChart(name);
929
1101
  return { success: true, message: `Cache invalidated for chart: ${name}` };
@@ -932,7 +1104,8 @@ async function chartRoutes(fastify, options) {
932
1104
  const { chartName, paramName } = request.params;
933
1105
  const chart = configLoader.getChartByName(chartName);
934
1106
  if (!chart) {
935
- return reply.status(404).send({ error: `Chart not found: ${chartName}`, errorType: "not_found" });
1107
+ const resp = chartNotFoundResponse(configLoader, chartName);
1108
+ return reply.status(resp.status).send(resp.body);
936
1109
  }
937
1110
  const param = chart.parameters?.find((p) => p.name === paramName);
938
1111
  if (!param) {
@@ -954,10 +1127,73 @@ async function chartRoutes(fastify, options) {
954
1127
  });
955
1128
  }
956
1129
 
957
- // ../server/dist/routes/dashboards.js
958
- import { writeFile } from "fs/promises";
1130
+ // ../server/dist/routes/chart-events.js
1131
+ import { readFile as readFile2, writeFile } from "fs/promises";
959
1132
  import { join as join3 } from "path";
960
- import { stringify as stringifyYaml } from "yaml";
1133
+ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
1134
+ async function readEventsFile(projectDir) {
1135
+ const eventsPath = join3(projectDir, "events.yaml");
1136
+ try {
1137
+ const content = await readFile2(eventsPath, "utf-8");
1138
+ const parsed = parseYaml2(content);
1139
+ const result = EventsFileSchema.safeParse(parsed);
1140
+ return result.success ? result.data.events : [];
1141
+ } catch {
1142
+ return [];
1143
+ }
1144
+ }
1145
+ async function writeEventsFile(projectDir, events) {
1146
+ const eventsPath = join3(projectDir, "events.yaml");
1147
+ await writeFile(eventsPath, stringifyYaml({ events }));
1148
+ }
1149
+ async function chartEventsRoutes(fastify, options) {
1150
+ const { configLoader, projectDir } = options;
1151
+ fastify.get("/api/chart-events", async () => {
1152
+ return { events: configLoader.getEvents() };
1153
+ });
1154
+ fastify.post("/api/chart-events", async (request, reply) => {
1155
+ const result = EventSchema.safeParse(request.body);
1156
+ if (!result.success) {
1157
+ return reply.status(400).send({ error: result.error.message });
1158
+ }
1159
+ const events = await readEventsFile(projectDir);
1160
+ events.push(result.data);
1161
+ await writeEventsFile(projectDir, events);
1162
+ await configLoader.load();
1163
+ return reply.status(201).send({ event: result.data, index: events.length - 1 });
1164
+ });
1165
+ fastify.delete("/api/chart-events/:index", async (request, reply) => {
1166
+ const index = parseInt(request.params.index, 10);
1167
+ const events = await readEventsFile(projectDir);
1168
+ if (isNaN(index) || index < 0 || index >= events.length) {
1169
+ return reply.status(404).send({ error: "Event not found" });
1170
+ }
1171
+ events.splice(index, 1);
1172
+ await writeEventsFile(projectDir, events);
1173
+ await configLoader.load();
1174
+ return { success: true };
1175
+ });
1176
+ fastify.put("/api/chart-events/:index", async (request, reply) => {
1177
+ const index = parseInt(request.params.index, 10);
1178
+ const events = await readEventsFile(projectDir);
1179
+ if (isNaN(index) || index < 0 || index >= events.length) {
1180
+ return reply.status(404).send({ error: "Event not found" });
1181
+ }
1182
+ const result = EventSchema.safeParse(request.body);
1183
+ if (!result.success) {
1184
+ return reply.status(400).send({ error: result.error.message });
1185
+ }
1186
+ events[index] = result.data;
1187
+ await writeEventsFile(projectDir, events);
1188
+ await configLoader.load();
1189
+ return { event: result.data };
1190
+ });
1191
+ }
1192
+
1193
+ // ../server/dist/routes/dashboards.js
1194
+ import { writeFile as writeFile2 } from "fs/promises";
1195
+ import { join as join4 } from "path";
1196
+ import { stringify as stringifyYaml2 } from "yaml";
961
1197
  function classifyError2(error2) {
962
1198
  const msg = error2 instanceof Error ? error2.message : String(error2);
963
1199
  const lower = msg.toLowerCase();
@@ -969,6 +1205,22 @@ function classifyError2(error2) {
969
1205
  }
970
1206
  return "query_error";
971
1207
  }
1208
+ function dashboardNotFoundResponse(configLoader, name) {
1209
+ const validationError = configLoader.getValidationError(name);
1210
+ if (validationError) {
1211
+ return {
1212
+ status: 422,
1213
+ body: {
1214
+ error: `Invalid dashboard config "${name}" (${validationError.file}): ${validationError.message}`,
1215
+ errorType: "validation_error"
1216
+ }
1217
+ };
1218
+ }
1219
+ return {
1220
+ status: 404,
1221
+ body: { error: `Dashboard not found: ${name}`, errorType: "not_found" }
1222
+ };
1223
+ }
972
1224
  async function dashboardRoutes(fastify, options) {
973
1225
  const { configLoader, gitService, projectDir } = options;
974
1226
  fastify.get("/api/dashboards", async () => {
@@ -984,7 +1236,8 @@ async function dashboardRoutes(fastify, options) {
984
1236
  const { id } = request.params;
985
1237
  const dashboard = configLoader.getDashboardByName(id);
986
1238
  if (!dashboard) {
987
- return reply.status(404).send({ error: `Dashboard not found: ${id}`, errorType: "not_found" });
1239
+ const resp = dashboardNotFoundResponse(configLoader, id);
1240
+ return reply.status(resp.status).send(resp.body);
988
1241
  }
989
1242
  const currentBranch = await gitService.getCurrentBranch();
990
1243
  return {
@@ -1018,8 +1271,8 @@ async function dashboardRoutes(fastify, options) {
1018
1271
  }
1019
1272
  updated = { ...dashboard, layout };
1020
1273
  }
1021
- const filePath = configLoader.getDashboardFilePath(id) ?? join3(projectDir, "dashboards", `${id}.yaml`);
1022
- await writeFile(filePath, stringifyYaml(updated));
1274
+ const filePath = configLoader.getDashboardFilePath(id) ?? join4(projectDir, "dashboards", `${id}.yaml`);
1275
+ await writeFile2(filePath, stringifyYaml2(updated));
1023
1276
  await configLoader.load();
1024
1277
  const commitMessage = message || `Update ${dashboard.title} layout`;
1025
1278
  const result = await gitService.commitAndPush(commitMessage);
@@ -1066,6 +1319,10 @@ async function dashboardRoutes(fastify, options) {
1066
1319
  const results = await Promise.all(chartRefs.map(async (chartRef) => {
1067
1320
  const chart = configLoader.getChartByName(chartRef);
1068
1321
  if (!chart) {
1322
+ const validationError = configLoader.getValidationError(chartRef);
1323
+ if (validationError) {
1324
+ return { chart: chartRef, error: `Invalid chart config: ${validationError.message}`, errorType: "validation_error" };
1325
+ }
1069
1326
  return { chart: chartRef, error: "Chart not found", errorType: "not_found" };
1070
1327
  }
1071
1328
  try {
@@ -1132,6 +1389,143 @@ async function dashboardRoutes(fastify, options) {
1132
1389
  });
1133
1390
  }
1134
1391
 
1392
+ // ../server/dist/routes/semantic.js
1393
+ async function semanticRoutes(fastify, options) {
1394
+ const { semanticService, queryService, projectDir } = options;
1395
+ fastify.get("/api/semantic/meta", async (_request2, reply) => {
1396
+ const model = semanticService.getModel();
1397
+ if (!model) {
1398
+ return reply.status(503).send({
1399
+ error: "Semantic model not built. Ensure catalog.json exists or models are loaded.",
1400
+ errorType: "not_ready"
1401
+ });
1402
+ }
1403
+ return model;
1404
+ });
1405
+ fastify.post("/api/semantic/query", async (request, reply) => {
1406
+ const parseResult = SemanticQuerySchema.safeParse(request.body);
1407
+ if (!parseResult.success) {
1408
+ return reply.status(400).send({
1409
+ error: parseResult.error.issues.map((i) => i.message).join("; "),
1410
+ errorType: "validation_error"
1411
+ });
1412
+ }
1413
+ const query = parseResult.data;
1414
+ try {
1415
+ const result = semanticService.compileQuery(query);
1416
+ if (queryService && typeof queryService.executeRawSql === "function") {
1417
+ try {
1418
+ const startTime = Date.now();
1419
+ const queryResult = await queryService.executeRawSql(result.sql);
1420
+ const duration = Date.now() - startTime;
1421
+ return {
1422
+ columns: queryResult.columns,
1423
+ rows: queryResult.rows,
1424
+ meta: {
1425
+ compiled_sql: result.sql,
1426
+ duration_ms: duration,
1427
+ tier: result.tier,
1428
+ entity: result.entity,
1429
+ row_count: queryResult.rows.length
1430
+ }
1431
+ };
1432
+ } catch (execError) {
1433
+ const message = execError instanceof Error ? execError.message : String(execError);
1434
+ return reply.status(500).send({
1435
+ error: message,
1436
+ errorType: "query_error",
1437
+ compiled_sql: result.sql
1438
+ });
1439
+ }
1440
+ }
1441
+ return {
1442
+ columns: [],
1443
+ rows: [],
1444
+ meta: {
1445
+ compiled_sql: result.sql,
1446
+ tier: result.tier,
1447
+ entity: result.entity
1448
+ }
1449
+ };
1450
+ } catch (error2) {
1451
+ if (error2 instanceof SemanticValidationError) {
1452
+ return reply.status(400).send({
1453
+ error: error2.message,
1454
+ errorType: "validation_error",
1455
+ code: error2.code,
1456
+ available: error2.available
1457
+ });
1458
+ }
1459
+ throw error2;
1460
+ }
1461
+ });
1462
+ fastify.post("/api/semantic/save-model", async (request, reply) => {
1463
+ const { name, sql, path: filePath } = request.body;
1464
+ if (!name || !sql || !filePath) {
1465
+ return reply.status(400).send({
1466
+ error: "name, sql, and path are required",
1467
+ errorType: "validation_error"
1468
+ });
1469
+ }
1470
+ const { writeFileSync, mkdirSync } = await import("fs");
1471
+ const { join: join7, dirname: dirname3 } = await import("path");
1472
+ const fullPath = join7(projectDir, filePath);
1473
+ mkdirSync(dirname3(fullPath), { recursive: true });
1474
+ writeFileSync(fullPath, sql, "utf-8");
1475
+ return { saved: true, path: filePath };
1476
+ });
1477
+ fastify.post("/api/semantic/rebuild", async () => {
1478
+ semanticService.buildFromDisk();
1479
+ const model = semanticService.getModel();
1480
+ return {
1481
+ rebuilt: true,
1482
+ entity_count: model?.entities.length ?? 0,
1483
+ generated_at: model?.generated_at
1484
+ };
1485
+ });
1486
+ }
1487
+
1488
+ // ../server/dist/services/semantic-service.js
1489
+ import { readFileSync, existsSync } from "fs";
1490
+ import { join as join5 } from "path";
1491
+ var SemanticService = class {
1492
+ model = null;
1493
+ builder;
1494
+ compiler = new SemanticQueryCompiler();
1495
+ projectDir;
1496
+ constructor(options) {
1497
+ this.projectDir = options.projectDir;
1498
+ this.builder = new SemanticModelBuilder(options.config);
1499
+ }
1500
+ getModel() {
1501
+ return this.model;
1502
+ }
1503
+ build(input) {
1504
+ this.model = this.builder.build(input);
1505
+ }
1506
+ buildFromDisk(models, chartHints) {
1507
+ const catalogPath = join5(this.projectDir, ".yamchart", "catalog.json");
1508
+ let catalog;
1509
+ if (existsSync(catalogPath)) {
1510
+ const raw = readFileSync(catalogPath, "utf-8");
1511
+ catalog = JSON.parse(raw);
1512
+ }
1513
+ this.build({ catalog, models, chartHints });
1514
+ }
1515
+ compileQuery(query) {
1516
+ if (!this.model) {
1517
+ throw new Error("Semantic model not built. Run build() first.");
1518
+ }
1519
+ validateSemanticQuery(this.model, query);
1520
+ const sql = this.compiler.compile(this.model, query);
1521
+ return {
1522
+ sql,
1523
+ tier: "direct",
1524
+ entity: query.model
1525
+ };
1526
+ }
1527
+ };
1528
+
1135
1529
  // ../server/dist/services/slack-notifier.js
1136
1530
  var MAX_TABLE_ROWS = 10;
1137
1531
  var OPERATOR_SYMBOLS = {
@@ -4122,8 +4516,8 @@ var RealtimeChannel = class _RealtimeChannel {
4122
4516
  _trigger(type, payload, ref) {
4123
4517
  var _a, _b;
4124
4518
  const typeLower = type.toLocaleLowerCase();
4125
- const { close, error: error2, leave, join: join5 } = CHANNEL_EVENTS;
4126
- const events = [close, error2, leave, join5];
4519
+ const { close, error: error2, leave, join: join7 } = CHANNEL_EVENTS;
4520
+ const events = [close, error2, leave, join7];
4127
4521
  if (ref && events.indexOf(typeLower) >= 0 && ref !== this._joinRef()) {
4128
4522
  return;
4129
4523
  }
@@ -13352,6 +13746,10 @@ async function publicRoutes(fastify, options) {
13352
13746
  return;
13353
13747
  const chart = configLoader.getChartByName(name);
13354
13748
  if (!chart) {
13749
+ const validationError = configLoader.getValidationError(name);
13750
+ if (validationError) {
13751
+ return reply.status(422).send({ error: `Invalid chart config "${name}" (${validationError.file}): ${validationError.message}` });
13752
+ }
13355
13753
  return reply.status(404).send({ error: `Chart not found: ${name}` });
13356
13754
  }
13357
13755
  try {
@@ -13390,6 +13788,10 @@ async function publicRoutes(fastify, options) {
13390
13788
  return;
13391
13789
  const chart = configLoader.getChartByName(name);
13392
13790
  if (!chart) {
13791
+ const validationError = configLoader.getValidationError(name);
13792
+ if (validationError) {
13793
+ return reply.status(422).send({ error: `Invalid chart config "${name}" (${validationError.file}): ${validationError.message}` });
13794
+ }
13393
13795
  return reply.status(404).send({ error: `Chart not found: ${name}` });
13394
13796
  }
13395
13797
  return {
@@ -13407,6 +13809,10 @@ async function publicRoutes(fastify, options) {
13407
13809
  return;
13408
13810
  const dashboard = configLoader.getDashboardByName(id);
13409
13811
  if (!dashboard) {
13812
+ const validationError = configLoader.getValidationError(id);
13813
+ if (validationError) {
13814
+ return reply.status(422).send({ error: `Invalid dashboard config "${id}" (${validationError.file}): ${validationError.message}` });
13815
+ }
13410
13816
  return reply.status(404).send({ error: `Dashboard not found: ${id}` });
13411
13817
  }
13412
13818
  return {
@@ -13457,8 +13863,13 @@ async function publicRoutes(fastify, options) {
13457
13863
  return { name, error: "Access denied" };
13458
13864
  }
13459
13865
  const chart = configLoader.getChartByName(name);
13460
- if (!chart)
13866
+ if (!chart) {
13867
+ const validationError = configLoader.getValidationError(name);
13868
+ if (validationError) {
13869
+ return { name, error: `Invalid chart config "${name}" (${validationError.file}): ${validationError.message}` };
13870
+ }
13461
13871
  return { name, error: `Chart not found: ${name}` };
13872
+ }
13462
13873
  try {
13463
13874
  const result = await queryService.executeChart(chart, params);
13464
13875
  const response = {
@@ -13608,7 +14019,7 @@ async function chatRoutes(fastify, options) {
13608
14019
  systemParts.push("Output ONLY the new text to insert. Do NOT repeat the existing content.");
13609
14020
  }
13610
14021
  const systemPrompt = systemParts.join("\n");
13611
- const { AnthropicProvider } = await import("./dist-YTGUIBKG.js");
14022
+ const { AnthropicProvider } = await import("./dist-WDTDQDTX.js");
13612
14023
  const provider = new AnthropicProvider(apiKey);
13613
14024
  const response = await provider.chat({
13614
14025
  system: systemPrompt,
@@ -13647,6 +14058,12 @@ async function eventsRoute(fastify) {
13647
14058
  });
13648
14059
  });
13649
14060
  }
14061
+ function closeAllClients() {
14062
+ for (const client of clients) {
14063
+ client.end();
14064
+ }
14065
+ clients.clear();
14066
+ }
13650
14067
  function broadcastConfigChange() {
13651
14068
  const data = JSON.stringify({ type: "config-change", timestamp: Date.now() });
13652
14069
  const message = `event: config-change
@@ -13659,17 +14076,18 @@ data: ${data}
13659
14076
  }
13660
14077
 
13661
14078
  // ../server/dist/server.js
13662
- import { join as join4, dirname as dirname2 } from "path";
14079
+ import { join as join6, dirname as dirname2 } from "path";
13663
14080
  import { fileURLToPath } from "url";
13664
- import { access as access2, readFile as readFile2 } from "fs/promises";
14081
+ import { access as access2, readFile as readFile3 } from "fs/promises";
14082
+ import { watch as fsWatch } from "fs";
13665
14083
  var __dirname = dirname2(fileURLToPath(import.meta.url));
13666
- var packageJsonPath = join4(__dirname, "..", "package.json");
13667
- var packageJson = JSON.parse(await readFile2(packageJsonPath, "utf-8"));
14084
+ var packageJsonPath = join6(__dirname, "..", "package.json");
14085
+ var packageJson = JSON.parse(await readFile3(packageJsonPath, "utf-8"));
13668
14086
  var VERSION = packageJson.version;
13669
14087
  async function enrichRefsFromCatalog(projectDir, refs) {
13670
14088
  try {
13671
- const catalogPath = join4(projectDir, ".yamchart", "catalog.json");
13672
- const catalogData = JSON.parse(await readFile2(catalogPath, "utf-8"));
14089
+ const catalogPath = join6(projectDir, ".yamchart", "catalog.json");
14090
+ const catalogData = JSON.parse(await readFile3(catalogPath, "utf-8"));
13673
14091
  for (const model of catalogData.models ?? []) {
13674
14092
  if (model.name && model.table) {
13675
14093
  refs[model.name] = model.table;
@@ -13679,7 +14097,7 @@ async function enrichRefsFromCatalog(projectDir, refs) {
13679
14097
  }
13680
14098
  }
13681
14099
  async function createServer(options) {
13682
- const { projectDir, port = 3001, host = "0.0.0.0", watch: watch2 = false, serveStatic = process.env.NODE_ENV === "production", staticDir = join4(__dirname, "public"), auth } = options;
14100
+ const { projectDir, port = 3001, host = "0.0.0.0", watch: watch2 = false, serveStatic = process.env.NODE_ENV === "production", staticDir = join6(__dirname, "public"), auth } = options;
13683
14101
  if (auth) {
13684
14102
  initAuthServer(auth.supabaseUrl, auth.supabaseServiceKey);
13685
14103
  }
@@ -13734,6 +14152,12 @@ async function createServer(options) {
13734
14152
  models,
13735
14153
  refs
13736
14154
  });
14155
+ const semanticService = new SemanticService({
14156
+ projectDir,
14157
+ config: project.semantic
14158
+ });
14159
+ semanticService.buildFromDisk();
14160
+ fastify.log.info(`Semantic model built: ${semanticService.getModel()?.entities.length ?? 0} entities`);
13737
14161
  const notifier = new SlackNotifier();
13738
14162
  const baseUrl = project.defaults?.base_url ?? process.env.BASE_URL ?? void 0;
13739
14163
  const schedulerService = new SchedulerService(queryService, configLoader, notifier, baseUrl);
@@ -13767,6 +14191,7 @@ async function createServer(options) {
13767
14191
  });
13768
14192
  }
13769
14193
  });
14194
+ await fastify.register(semanticRoutes, { semanticService, queryService, projectDir });
13770
14195
  if (authDb) {
13771
14196
  const secureCookies = process.env.NODE_ENV === "production";
13772
14197
  const sessionTtlMs = options.localAuth.sessionTtlMs;
@@ -13802,6 +14227,7 @@ async function createServer(options) {
13802
14227
  protectedRoutes.addHook("preHandler", orgMiddleware);
13803
14228
  await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService });
13804
14229
  await protectedRoutes.register(chartRoutes, { configLoader, queryService });
14230
+ await protectedRoutes.register(chartEventsRoutes, { configLoader, projectDir });
13805
14231
  await protectedRoutes.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
13806
14232
  await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir });
13807
14233
  });
@@ -13811,6 +14237,7 @@ async function createServer(options) {
13811
14237
  protectedRoutes.addHook("preHandler", localMiddleware);
13812
14238
  await protectedRoutes.register(configRoutes, { configLoader, projectDir, queryService });
13813
14239
  await protectedRoutes.register(chartRoutes, { configLoader, queryService });
14240
+ await protectedRoutes.register(chartEventsRoutes, { configLoader, projectDir });
13814
14241
  await protectedRoutes.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
13815
14242
  await protectedRoutes.register(chatRoutes, { configLoader, queryService, projectDir });
13816
14243
  await protectedRoutes.register(sharesRoutes, { authDb });
@@ -13818,10 +14245,11 @@ async function createServer(options) {
13818
14245
  } else {
13819
14246
  await fastify.register(configRoutes, { configLoader, projectDir, queryService });
13820
14247
  await fastify.register(chartRoutes, { configLoader, queryService });
14248
+ await fastify.register(chartEventsRoutes, { configLoader, projectDir });
13821
14249
  await fastify.register(dashboardRoutes, { configLoader, gitService, projectDir, queryService });
13822
14250
  await fastify.register(chatRoutes, { configLoader, queryService, projectDir });
13823
14251
  }
13824
- const assetsDir = join4(projectDir, "assets");
14252
+ const assetsDir = join6(projectDir, "assets");
13825
14253
  try {
13826
14254
  await access2(assetsDir);
13827
14255
  await fastify.register(fastifyStatic, {
@@ -13832,10 +14260,10 @@ async function createServer(options) {
13832
14260
  } catch {
13833
14261
  }
13834
14262
  if (project.theme?.customCss) {
13835
- const cssPath = join4(projectDir, project.theme.customCss);
14263
+ const cssPath = join6(projectDir, project.theme.customCss);
13836
14264
  fastify.get("/api/theme/custom.css", async (_request2, reply) => {
13837
14265
  try {
13838
- const content = await readFile2(cssPath, "utf-8");
14266
+ const content = await readFile3(cssPath, "utf-8");
13839
14267
  return reply.type("text/css").send(content);
13840
14268
  } catch {
13841
14269
  return reply.status(404).send("");
@@ -13873,6 +14301,7 @@ async function createServer(options) {
13873
14301
  await enrichRefsFromCatalog(projectDir, refs2);
13874
14302
  return { models: models2, refs: refs2 };
13875
14303
  }
14304
+ let catalogWatcher = null;
13876
14305
  if (watch2) {
13877
14306
  await fastify.register(eventsRoute);
13878
14307
  configLoader.startWatching();
@@ -13886,8 +14315,18 @@ async function createServer(options) {
13886
14315
  if (newSchedules.length > 0) {
13887
14316
  schedulerService.start(newSchedules);
13888
14317
  }
14318
+ semanticService.buildFromDisk();
14319
+ fastify.log.info(`Semantic model rebuilt: ${semanticService.getModel()?.entities.length ?? 0} entities`);
13889
14320
  broadcastConfigChange();
13890
14321
  });
14322
+ const catalogPath = join6(projectDir, ".yamchart", "catalog.json");
14323
+ try {
14324
+ catalogWatcher = fsWatch(catalogPath, () => {
14325
+ semanticService.buildFromDisk();
14326
+ fastify.log.info(`Semantic model rebuilt (catalog change): ${semanticService.getModel()?.entities.length ?? 0} entities`);
14327
+ });
14328
+ } catch {
14329
+ }
13891
14330
  }
13892
14331
  return {
13893
14332
  fastify,
@@ -13915,6 +14354,8 @@ async function createServer(options) {
13915
14354
  await configLoader.stop();
13916
14355
  await connector.disconnect();
13917
14356
  authDb?.close();
14357
+ catalogWatcher?.close();
14358
+ closeAllClients();
13918
14359
  await fastify.close();
13919
14360
  }
13920
14361
  };
@@ -13942,9 +14383,9 @@ async function runDevServer(projectDir, options) {
13942
14383
  let usesBrowserAuth = false;
13943
14384
  let localAuth;
13944
14385
  try {
13945
- const { readFileSync } = await import("fs");
14386
+ const { readFileSync: readFileSync2 } = await import("fs");
13946
14387
  const { parse } = await import("yaml");
13947
- const raw = readFileSync(resolve(projectDir, "yamchart.yaml"), "utf-8");
14388
+ const raw = readFileSync2(resolve(projectDir, "yamchart.yaml"), "utf-8");
13948
14389
  const projectConfig = parse(raw);
13949
14390
  if (projectConfig?.auth?.enabled) {
13950
14391
  localAuth = {
@@ -13957,7 +14398,7 @@ async function runDevServer(projectDir, options) {
13957
14398
  const targetConn = process.env.YAMCHART_CONNECTION || projectConfig?.defaults?.connection;
13958
14399
  if (targetConn) {
13959
14400
  try {
13960
- const connRaw = readFileSync(resolve(connectionDir, `${targetConn}.yaml`), "utf-8");
14401
+ const connRaw = readFileSync2(resolve(connectionDir, `${targetConn}.yaml`), "utf-8");
13961
14402
  const connConfig = parse(connRaw);
13962
14403
  if (connConfig?.auth?.type === "externalbrowser") {
13963
14404
  usesBrowserAuth = true;
@@ -14023,4 +14464,4 @@ async function runDevServer(projectDir, options) {
14023
14464
  export {
14024
14465
  runDevServer
14025
14466
  };
14026
- //# sourceMappingURL=dev-EUWIRL4N.js.map
14467
+ //# sourceMappingURL=dev-MRZ76V74.js.map