@malloy-publisher/server 0.0.382-dev → 0.2.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 (175) hide show
  1. package/README.docker.md +145 -20
  2. package/README.md +65 -12
  3. package/dist/app/api-doc.html +5 -0
  4. package/dist/app/api-doc.yaml +3929 -534
  5. package/dist/app/assets/EnvironmentPage-CHq5BF4l.js +1 -0
  6. package/dist/app/assets/HomePage-CIp-pJCa.js +1 -0
  7. package/dist/app/assets/LightMode-mHHPjEeA.js +1 -0
  8. package/dist/app/assets/MainPage-CylxO5Yu.js +2 -0
  9. package/dist/app/assets/MaterializationsPage-a0vjqxib.js +1 -0
  10. package/dist/app/assets/ModelPage-Cz-MrSZQ.js +1 -0
  11. package/dist/app/assets/PackagePage-Cdg5D7AN.js +1 -0
  12. package/dist/app/assets/RouteError-B5SIyn6G.js +1 -0
  13. package/dist/app/assets/ThemeEditorPage-B62rRc-y.js +1 -0
  14. package/dist/app/assets/WorkbookPage-45cG1Gx6.js +1 -0
  15. package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-DyU47reh.es-DY9b3WjS.js} +1 -1
  16. package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
  17. package/dist/app/assets/index-5eLCcNmP.css +1 -0
  18. package/dist/app/assets/index-C5ClsnS7.js +1774 -0
  19. package/dist/app/assets/index-DF9tVqwN.js +1 -0
  20. package/dist/app/assets/index-DSoCUF-f.js +2725 -0
  21. package/dist/app/assets/index-DgVRz2wm.js +2 -0
  22. package/dist/app/assets/index-P-pF8nU5.js +632 -0
  23. package/dist/app/assets/index-w_0OQJgZ.js +23 -0
  24. package/dist/app/index.html +8 -4
  25. package/dist/cpufeatures-1yrn0vtw.node +0 -0
  26. package/dist/default-publisher.config.json +23 -0
  27. package/dist/{instrumentation.js → instrumentation.mjs} +10631 -10573
  28. package/dist/package_load_worker.mjs +15219 -0
  29. package/dist/runtime/publisher.js +331 -0
  30. package/dist/{server.js → server.mjs} +141276 -74705
  31. package/dist/sshcrypto-vd2k5hq9.node +0 -0
  32. package/package.json +41 -28
  33. package/publisher.config.example.bigquery.json +33 -0
  34. package/publisher.config.example.duckdb.json +23 -0
  35. package/.eslintrc.json +0 -44
  36. package/.prettierignore +0 -1
  37. package/.prettierrc +0 -4
  38. package/build.ts +0 -38
  39. package/dist/app/assets/HomePage-Dn3E4CuB.js +0 -1
  40. package/dist/app/assets/MainPage-BzB3yoqi.js +0 -2
  41. package/dist/app/assets/ModelPage-C9O_sAXT.js +0 -1
  42. package/dist/app/assets/PackagePage-DcxKEjBX.js +0 -1
  43. package/dist/app/assets/ProjectPage-BDj307rF.js +0 -1
  44. package/dist/app/assets/RouteError-DAShbVCG.js +0 -1
  45. package/dist/app/assets/WorkbookPage-Cs_XYEaB.js +0 -1
  46. package/dist/app/assets/index-15BOvhp0.js +0 -456
  47. package/dist/app/assets/index-Bb2jqquW.js +0 -676
  48. package/dist/app/assets/index-CMlGQMcl.css +0 -1
  49. package/dist/app/assets/index-D68X76-7.js +0 -1276
  50. package/dist/app/assets/index.umd-DGBekgSu.js +0 -1145
  51. package/dxt/malloy_bridge.py +0 -354
  52. package/dxt/manifest.json +0 -22
  53. package/eslint.config.mjs +0 -76
  54. package/k6-tests/README.md +0 -144
  55. package/k6-tests/bun.lock +0 -797
  56. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  57. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  58. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  59. package/k6-tests/load-test/load-test-crud.ts +0 -142
  60. package/k6-tests/load-test/load-test.ts +0 -784
  61. package/k6-tests/package.json +0 -27
  62. package/k6-tests/scripts/run-with-otel.sh +0 -50
  63. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  64. package/k6-tests/tsconfig.json +0 -29
  65. package/k6-tests/utils/client_factory.ts +0 -128
  66. package/k6-tests/utils/common.ts +0 -489
  67. package/k6-tests/utils/logger.ts +0 -62
  68. package/k6-tests/utils/types.d.ts +0 -35
  69. package/malloy_mcp.dxt +0 -0
  70. package/publisher.config.json +0 -33
  71. package/src/config.spec.ts +0 -850
  72. package/src/config.ts +0 -305
  73. package/src/constants.ts +0 -10
  74. package/src/controller/compile.controller.ts +0 -35
  75. package/src/controller/connection.controller.ts +0 -515
  76. package/src/controller/database.controller.ts +0 -21
  77. package/src/controller/manifest.controller.ts +0 -29
  78. package/src/controller/materialization.controller.ts +0 -125
  79. package/src/controller/model.controller.ts +0 -107
  80. package/src/controller/package.controller.ts +0 -182
  81. package/src/controller/query.controller.ts +0 -60
  82. package/src/controller/watch-mode.controller.ts +0 -98
  83. package/src/data_styles.ts +0 -71
  84. package/src/dto/connection.dto.spec.ts +0 -131
  85. package/src/dto/connection.dto.ts +0 -174
  86. package/src/dto/index.ts +0 -2
  87. package/src/dto/package.dto.spec.ts +0 -42
  88. package/src/dto/package.dto.ts +0 -12
  89. package/src/dto/validate.spec.ts +0 -76
  90. package/src/dto/validate.ts +0 -31
  91. package/src/errors.ts +0 -116
  92. package/src/health.ts +0 -189
  93. package/src/instrumentation.ts +0 -148
  94. package/src/logger.ts +0 -154
  95. package/src/mcp/error_messages.ts +0 -199
  96. package/src/mcp/handler_utils.ts +0 -239
  97. package/src/mcp/mcp_constants.ts +0 -22
  98. package/src/mcp/prompts/handlers.ts +0 -84
  99. package/src/mcp/prompts/index.ts +0 -11
  100. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  101. package/src/mcp/prompts/prompt_service.ts +0 -67
  102. package/src/mcp/prompts/utils.ts +0 -62
  103. package/src/mcp/resource_metadata.ts +0 -47
  104. package/src/mcp/resources/model_resource.ts +0 -153
  105. package/src/mcp/resources/notebook_resource.ts +0 -137
  106. package/src/mcp/resources/package_resource.ts +0 -370
  107. package/src/mcp/resources/project_resource.ts +0 -184
  108. package/src/mcp/resources/query_resource.ts +0 -117
  109. package/src/mcp/resources/source_resource.ts +0 -141
  110. package/src/mcp/resources/view_resource.ts +0 -136
  111. package/src/mcp/server.ts +0 -62
  112. package/src/mcp/tools/discovery_tools.ts +0 -258
  113. package/src/mcp/tools/execute_query_tool.ts +0 -265
  114. package/src/server.ts +0 -1195
  115. package/src/service/connection.spec.ts +0 -1331
  116. package/src/service/connection.ts +0 -1511
  117. package/src/service/connection_service.spec.ts +0 -641
  118. package/src/service/connection_service.ts +0 -181
  119. package/src/service/db_utils.spec.ts +0 -714
  120. package/src/service/db_utils.ts +0 -1227
  121. package/src/service/filter.spec.ts +0 -447
  122. package/src/service/filter.ts +0 -337
  123. package/src/service/filter_integration.spec.ts +0 -825
  124. package/src/service/gcs_s3_utils.ts +0 -427
  125. package/src/service/manifest_service.spec.ts +0 -201
  126. package/src/service/manifest_service.ts +0 -106
  127. package/src/service/materialization_service.spec.ts +0 -648
  128. package/src/service/materialization_service.ts +0 -929
  129. package/src/service/materialized_table_gc.spec.ts +0 -383
  130. package/src/service/materialized_table_gc.ts +0 -279
  131. package/src/service/model.spec.ts +0 -257
  132. package/src/service/model.ts +0 -1069
  133. package/src/service/package.spec.ts +0 -236
  134. package/src/service/package.ts +0 -463
  135. package/src/service/project.ts +0 -681
  136. package/src/service/project_compile.spec.ts +0 -197
  137. package/src/service/project_store.spec.ts +0 -859
  138. package/src/service/project_store.ts +0 -1531
  139. package/src/service/quoting.ts +0 -41
  140. package/src/service/resolve_project.ts +0 -13
  141. package/src/storage/BaseRepository.ts +0 -31
  142. package/src/storage/DatabaseInterface.ts +0 -180
  143. package/src/storage/StorageManager.mock.ts +0 -59
  144. package/src/storage/StorageManager.ts +0 -210
  145. package/src/storage/duckdb/ConnectionRepository.ts +0 -141
  146. package/src/storage/duckdb/DuckDBConnection.ts +0 -177
  147. package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
  148. package/src/storage/duckdb/DuckDBRepository.ts +0 -224
  149. package/src/storage/duckdb/ManifestRepository.ts +0 -119
  150. package/src/storage/duckdb/MaterializationRepository.ts +0 -249
  151. package/src/storage/duckdb/PackageRepository.ts +0 -135
  152. package/src/storage/duckdb/ProjectRepository.ts +0 -148
  153. package/src/storage/duckdb/manifest_store.spec.ts +0 -133
  154. package/src/storage/duckdb/schema.ts +0 -147
  155. package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -146
  156. package/src/utils.ts +0 -13
  157. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  158. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  159. package/tests/fixtures/persist-test/publisher.json +0 -5
  160. package/tests/fixtures/publisher.config.json +0 -15
  161. package/tests/harness/e2e.ts +0 -96
  162. package/tests/harness/mcp_test_setup.ts +0 -344
  163. package/tests/harness/mocks.ts +0 -37
  164. package/tests/harness/rest_e2e.ts +0 -68
  165. package/tests/harness/uris.ts +0 -31
  166. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -470
  167. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -434
  168. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -663
  169. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -122
  170. package/tests/integration/mcp/setup.spec.ts +0 -5
  171. package/tests/unit/duckdb/attached_databases.test.ts +0 -1132
  172. package/tests/unit/ducklake/ducklake.test.ts +0 -950
  173. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  174. package/tests/unit/mcp/prompt_happy.test.ts +0 -51
  175. package/tsconfig.json +0 -32
@@ -1,148 +0,0 @@
1
- import { metrics } from "@opentelemetry/api";
2
- import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
3
- import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
4
- import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
5
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
6
- import {
7
- ExpressInstrumentation,
8
- ExpressLayerType,
9
- } from "@opentelemetry/instrumentation-express";
10
- import { resourceFromAttributes } from "@opentelemetry/resources";
11
- import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
12
- import { NodeSDK } from "@opentelemetry/sdk-node";
13
- import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
14
- import {
15
- ATTR_SERVICE_NAME,
16
- ATTR_SERVICE_VERSION,
17
- } from "@opentelemetry/semantic-conventions";
18
- import type { NextFunction, Request, Response } from "express";
19
- import { logger } from "./logger";
20
-
21
- let prometheusExporter: PrometheusExporter | null = null;
22
- let sdk: NodeSDK | null = null;
23
-
24
- export function getPrometheusMetricsHandler() {
25
- if (!prometheusExporter) {
26
- throw new Error("Prometheus exporter not initialized");
27
- }
28
- return (req: Request, res: Response) => {
29
- prometheusExporter!.getMetricsRequestHandler(req, res);
30
- };
31
- }
32
-
33
- /**
34
- * Shuts down the OpenTelemetry SDK gracefully.
35
- * Should be called during application shutdown.
36
- */
37
- export async function shutdownSDK(): Promise<void> {
38
- if (sdk) {
39
- await sdk.shutdown();
40
- sdk = null;
41
- }
42
- }
43
-
44
- function instrument() {
45
- const otelCollectorUrl = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
46
-
47
- prometheusExporter = new PrometheusExporter({
48
- preventServerStart: true,
49
- });
50
-
51
- const instrumentations = [
52
- getNodeAutoInstrumentations(),
53
- new ExpressInstrumentation({
54
- ignoreLayersType: [ExpressLayerType.MIDDLEWARE],
55
- ignoreLayers: [/\/health/, /\/metrics/],
56
- }),
57
- ];
58
-
59
- sdk = new NodeSDK({
60
- resource: resourceFromAttributes({
61
- [ATTR_SERVICE_NAME]: "publisher",
62
- [ATTR_SERVICE_VERSION]: "1.0.0",
63
- }),
64
- autoDetectResources: true,
65
- metricReader: prometheusExporter,
66
- instrumentations,
67
- ...(otelCollectorUrl && {
68
- spanProcessors: [
69
- new BatchSpanProcessor(
70
- new OTLPTraceExporter({
71
- url: `${otelCollectorUrl}/v1/traces`,
72
- }),
73
- ),
74
- ],
75
- logRecordProcessors: [
76
- new BatchLogRecordProcessor(
77
- new OTLPLogExporter({
78
- url: `${otelCollectorUrl}/v1/logs`,
79
- }),
80
- ),
81
- ],
82
- }),
83
- });
84
-
85
- sdk.start();
86
-
87
- if (otelCollectorUrl) {
88
- logger.info(
89
- `OpenTelemetry SDK initialized with OTLP collector at ${otelCollectorUrl}`,
90
- );
91
- } else {
92
- logger.info("OpenTelemetry SDK initialized with Prometheus metrics only");
93
- }
94
- }
95
-
96
- instrument();
97
-
98
- // --- HTTP metrics middleware ---
99
-
100
- const meter = metrics.getMeter("publisher");
101
-
102
- const httpRequestDuration = meter.createHistogram(
103
- "http_server_request_duration_ms",
104
- {
105
- description: "Duration of HTTP requests in milliseconds",
106
- unit: "ms",
107
- advice: {
108
- explicitBucketBoundaries: [
109
- 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 60000,
110
- ],
111
- },
112
- },
113
- );
114
-
115
- const httpRequestCount = meter.createCounter("http_server_requests_total", {
116
- description: "Total number of HTTP requests",
117
- });
118
-
119
- const IGNORED_PATHS = new Set([
120
- "/health",
121
- "/health/liveness",
122
- "/health/readiness",
123
- "/metrics",
124
- ]);
125
-
126
- export function httpMetricsMiddleware(
127
- req: Request,
128
- res: Response,
129
- next: NextFunction,
130
- ) {
131
- const start = performance.now();
132
-
133
- res.on("finish", () => {
134
- if (IGNORED_PATHS.has(req.path)) return;
135
-
136
- const duration = performance.now() - start;
137
- const attrs = {
138
- "http.method": req.method,
139
- "http.route": req.route?.path ?? req.path,
140
- "http.status_code": res.statusCode,
141
- };
142
-
143
- httpRequestDuration.record(duration, attrs);
144
- httpRequestCount.add(1, attrs);
145
- });
146
-
147
- next();
148
- }
package/src/logger.ts DELETED
@@ -1,154 +0,0 @@
1
- import { AxiosError } from "axios";
2
- import { RequestHandler } from "express";
3
- import winston from "winston";
4
-
5
- const isTelemetryEnabled = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
6
-
7
- // Determine log level from environment variable
8
- // Valid levels: error, warn, info, verbose, debug, silly
9
- // Default: 'debug'
10
- const VALID_LOG_LEVELS = ["error", "warn", "info", "verbose", "debug", "silly"];
11
- const getLogLevel = (): string => {
12
- if (process.env.LOG_LEVEL) {
13
- const logLevel = process.env.LOG_LEVEL.toLowerCase();
14
- if (VALID_LOG_LEVELS.includes(logLevel)) {
15
- return logLevel;
16
- } else {
17
- console.error(
18
- `Invalid log level: ${process.env.LOG_LEVEL}. Valid log levels are: ${VALID_LOG_LEVELS.join(", ")}. Defaulting to "debug".`,
19
- );
20
- }
21
- }
22
- return "debug";
23
- };
24
-
25
- export const logger = winston.createLogger({
26
- level: getLogLevel(),
27
- format: isTelemetryEnabled
28
- ? winston.format.combine(
29
- winston.format.uncolorize(),
30
- winston.format.timestamp(),
31
- winston.format.metadata({
32
- fillExcept: ["message", "level", "timestamp"],
33
- }),
34
- winston.format.json(),
35
- )
36
- : winston.format.combine(
37
- winston.format.colorize(),
38
- winston.format.simple(),
39
- ),
40
- transports: [new winston.transports.Console()],
41
- });
42
-
43
- //-
44
- /**
45
- * Extracts the trace ID from a W3C traceparent header.
46
- * Format: version-trace-id-parent-id-trace-flags
47
- * Example: 00-81f2264f363f1b5596c84ab29e6be171-83ef39df12ab6bab-01
48
- *
49
- * @param traceparent The traceparent header value
50
- * @returns The trace ID (32 hex characters) or undefined if invalid
51
- */
52
- function extractTraceIdFromTraceparent(
53
- traceparent: string | undefined,
54
- ): string | undefined {
55
- if (!traceparent) {
56
- return undefined;
57
- }
58
-
59
- // format of traceparent can either be: version-traceId-parentId-traceFlags or traceId
60
- const parts = traceparent.split("-");
61
- const traceId =
62
- parts.length >= 2 ? parts[1] : parts.length == 1 ? parts[0] : undefined;
63
- // Validate that the traceId is 32 hex characters
64
- if (traceId && traceId.length === 32 && /^[0-9a-fA-F]{32}$/.test(traceId)) {
65
- return traceId;
66
- }
67
-
68
- return undefined;
69
- }
70
-
71
- const DISABLE_RESPONSE_LOGGING =
72
- process.env.DISABLE_RESPONSE_LOGGING === "true" ||
73
- process.env.DISABLE_RESPONSE_LOGGING === "1";
74
-
75
- /**
76
- * Format duration in milliseconds to a human-readable string with unit
77
- * @param durationMs Duration in milliseconds
78
- * @returns Formatted string with 2 decimal places and unit (s or ms)
79
- */
80
- export function formatDuration(durationMs: number): string {
81
- // If duration is >= 1000ms, show in seconds, otherwise show in milliseconds
82
- if (durationMs >= 1000) {
83
- const seconds = durationMs / 1000;
84
- return `${seconds.toFixed(2)}s`;
85
- }
86
- return `${durationMs.toFixed(2)}ms`;
87
- }
88
-
89
- export const loggerMiddleware: RequestHandler = (req, res, next) => {
90
- const startTime = performance.now();
91
- const resJson = res.json;
92
- res.json = (body: unknown) => {
93
- res.locals.body = body;
94
- return resJson.call(res, body);
95
- };
96
- res.on("finish", () => {
97
- const endTime = performance.now();
98
- const durationMs = endTime - startTime;
99
-
100
- // Extract trace ID from traceparent header if present
101
- const traceparent = req.headers["traceparent"] as string | undefined;
102
- const traceId = extractTraceIdFromTraceparent(traceparent);
103
-
104
- const logMetadata: Record<string, unknown> = {
105
- statusCode: res.statusCode,
106
- duration: formatDuration(durationMs),
107
- payload: req.body,
108
- params: req.params,
109
- query: req.query,
110
- };
111
-
112
- // Only include response body if response logging is enabled
113
- if (!DISABLE_RESPONSE_LOGGING) {
114
- logMetadata.response = res.locals.body;
115
- }
116
-
117
- // Add traceId to log metadata if present
118
- if (traceId) {
119
- logMetadata.traceId = traceId;
120
- }
121
-
122
- // Skip logging for metrics and health endpoints to reduce log noise
123
- if (
124
- req.url !== "/metrics" &&
125
- req.url !== "/health" &&
126
- req.url !== "/health/liveness" &&
127
- req.url !== "/health/readiness"
128
- ) {
129
- logger.info(`${req.method} ${req.url}`, logMetadata);
130
- }
131
- });
132
- next();
133
- };
134
-
135
- export const logAxiosError = (error: AxiosError) => {
136
- if (error.response) {
137
- // The request was made and the server responded with a status code
138
- // that falls out of the range of 2xx
139
- logger.error("Axios server-side error", {
140
- url: error.response.config.url,
141
- status: error.response.status,
142
- headers: error.response.headers,
143
- data: error.response.data,
144
- });
145
- } else if (error.request) {
146
- // The request was made but no response was received
147
- // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
148
- // http.ClientRequest in node.js
149
- logger.error("Axios client-side error", { error: error.request });
150
- } else {
151
- // Something happened in setting up the request that triggered an Error
152
- logger.error("Axios unknown error", { error });
153
- }
154
- };
@@ -1,199 +0,0 @@
1
- export interface ErrorDetails {
2
- message: string;
3
- suggestions: string[];
4
- }
5
-
6
- export interface InvalidParamDetail {
7
- name: string;
8
- reason: string;
9
- value?: unknown;
10
- }
11
-
12
- export function getInvalidParamsError(
13
- params: InvalidParamDetail[],
14
- ): ErrorDetails {
15
- const reasons = params
16
- .map(
17
- (p) =>
18
- `${p.name}: ${p.reason}${p.value !== undefined ? ` (value: ${JSON.stringify(p.value)})` : ""}`,
19
- )
20
- .join("; ");
21
- return {
22
- message: `Invalid parameter(s) provided. ${reasons}`,
23
- suggestions: [
24
- "Check the parameter names and values against the prompt's argument definition.",
25
- "Ensure all required parameters are provided and have the correct data types.",
26
- "For URI parameters, ensure they are correctly formatted.",
27
- ],
28
- };
29
- }
30
-
31
- /**
32
- * Generates error details for a resource not found scenario.
33
- * @param resourceUriOrContext The URI that was not found, or a context string (e.g., "Package 'X'", "Model 'Y' in package 'X'").
34
- * @returns ErrorDetails object with message and suggestions.
35
- */
36
- export function getNotFoundError(resourceUriOrContext: string): ErrorDetails {
37
- const baseMessage = `Resource not found: ${resourceUriOrContext}`;
38
- const suggestions: string[] = [
39
- `Verify the identifier or URI (${resourceUriOrContext}) is spelled correctly and exists. Check capitalization and path separators.`,
40
- `If using a URI, ensure it follows the correct format (e.g., malloy://project/...) and includes the right path segments (e.g., /models/, /sources/, /queries/, /views/).`,
41
- ];
42
-
43
- suggestions.push(
44
- "Check if the resource exists and is correctly named in your Malloy project structure or the specific model file.",
45
- );
46
-
47
- return {
48
- message: baseMessage,
49
- suggestions: suggestions,
50
- };
51
- }
52
-
53
- /**
54
- * Generates generic error details for internal server errors.
55
- * @param operation The operation that failed (e.g., 'GetResource', 'ListResources').
56
- * @param error Optional: The underlying error object or message.
57
- * @returns ErrorDetails object.
58
- */
59
- export function getInternalError(
60
- operation: string,
61
- error?: unknown,
62
- ): ErrorDetails {
63
- const baseMessage = `An unexpected internal error occurred during ${operation}.`;
64
- const errorMessage = error instanceof Error ? error.message : String(error);
65
- return {
66
- message: error ? `${baseMessage}: ${errorMessage}` : baseMessage,
67
- suggestions: [
68
- "Try the request again later.",
69
- "If the problem persists, check server logs or contact support.",
70
- ],
71
- };
72
- }
73
-
74
- /**
75
- * Generates detailed error information for Malloy compilation or query execution errors.
76
- * @param operation The operation that failed (e.g., 'GetResource (model)', 'executeQuery').
77
- * @param modelIdentifier A path or URI identifying the model/notebook involved.
78
- * @param error The underlying Malloy error object or other error.
79
- * @returns ErrorDetails object.
80
- */
81
- export function getMalloyErrorDetails(
82
- operation: string,
83
- modelIdentifier: string,
84
- error: unknown,
85
- ): ErrorDetails {
86
- let baseMessage = `Error during ${operation} for resource '${modelIdentifier}'.`;
87
- const initialSuggestions: string[] = [
88
- `Verify the structure and syntax within the Malloy file: ${modelIdentifier}. Check for typos in source, query, or view names (e.g., 'source: my_source is ...', 'query: my_query is ...', 'view: my_view is { ... }'). See Model Structure: https://docs.malloydata.dev/documentation/language/statement`,
89
- "Ensure all referenced sources, queries, or views are correctly defined within the model or imported files using expected keywords (e.g., 'source:', 'query:', 'view:'). See Sources: https://docs.malloydata.dev/documentation/language/source",
90
- "Check the database connection configuration used by the model, if applicable.",
91
- "Consult the Malloy language documentation for syntax rules: https://docs.malloydata.dev/documentation/",
92
- ];
93
-
94
- let suggestions: string[] = [...initialSuggestions]; // Clone initial suggestions
95
- let refined = false; // Flag to track if specific suggestions were added
96
-
97
- // Attempt to extract more specific info if it's a MalloyError or similar
98
- if (error instanceof Error) {
99
- // Prepend the specific error message
100
- baseMessage = `Error during ${operation} for resource '${modelIdentifier}': ${error.message}`;
101
-
102
- // --- Add more specific suggestions based on common Malloy error patterns ---
103
- // Note: Order matters, check for more specific patterns first.
104
-
105
- const viewNotFoundMatch = error.message.match(
106
- /View '([^']+)' not found in source '([^']+)'/i,
107
- );
108
- const sourceNotFoundMatch = error.message.match(
109
- /Source '([^']+)' not found/i,
110
- );
111
- const queryNotFoundMatch = error.message.match(
112
- /Query '([^']+)' not found/i,
113
- );
114
- const referenceErrorMatch = error.message.match(
115
- /Reference to undefined object(?: '([^']+)')?/i,
116
- );
117
- const syntaxErrorMatch = error.message.match(
118
- /no viable alternative at input/i,
119
- );
120
- const connectionErrorMatch = error.message.match(
121
- /Cannot connect to database/i,
122
- );
123
- const fieldNotFoundMatch = error.message.match(
124
- /Field '([^']+)' not found in (source|query|view) '([^']+)'/i,
125
- );
126
- const invalidRequestMatch = error.message.match(
127
- /Invalid query request\\. Query OR queryName must be defined/i,
128
- );
129
-
130
- if (viewNotFoundMatch) {
131
- refined = true;
132
- const [, viewName, sourceName] = viewNotFoundMatch;
133
- suggestions.unshift(
134
- `Suggestion: View '${viewName}' was not found in source '${sourceName}'. Check the view name spelling or try requesting the resource details for the source URI (e.g., 'malloy://.../sources/${sourceName}') to see the list of available views. Views are defined within sources like 'source: ${sourceName} is ... extend { view: ${viewName} is { ... } }'.`,
135
- );
136
- } else if (sourceNotFoundMatch) {
137
- refined = true;
138
- const [, sourceName] = sourceNotFoundMatch;
139
- suggestions.unshift(
140
- `Suggestion: Source '${sourceName}' was not found or could not be accessed. Verify its definition (e.g., \`source: ${sourceName} is table('...')\` or \`duckdb.sql("...")\`) and ensure any associated connections are valid.`,
141
- `Suggestion: Check the spelling of '${sourceName}'. You can list sources in the model using ListResources('malloy://.../${modelIdentifier}').`,
142
- );
143
- } else if (queryNotFoundMatch) {
144
- refined = true;
145
- const [, queryName] = queryNotFoundMatch;
146
- suggestions.unshift(
147
- `Suggestion: Named query '${queryName}' was not found. Verify its definition (e.g., \`query: ${queryName} is source_name -> { ... }\`) within the model '${modelIdentifier}'.`,
148
- `Suggestion: Check the spelling of '${queryName}'. Ensure it's a named query (defined with \`query:\`), not a view. You can list named queries using ListResources('malloy://.../${modelIdentifier}').`,
149
- );
150
- } else if (fieldNotFoundMatch) {
151
- refined = true;
152
- const [, fieldName, fieldContextType, fieldContextName] =
153
- fieldNotFoundMatch;
154
- suggestions.unshift(
155
- `Suggestion: Field '${fieldName}' was not found in ${fieldContextType} '${fieldContextName}'. Check the spelling of the field name within the definition of '${fieldContextName}'.`,
156
- `Suggestion: Ensure the field is defined directly or inherited correctly in the '${fieldContextName}' ${fieldContextType}. You can inspect the ${fieldContextType} definition using GetResource.`,
157
- );
158
- } else if (referenceErrorMatch) {
159
- refined = true;
160
- const objectName = referenceErrorMatch[1] || "[unknown object]";
161
- suggestions.unshift(
162
- `Suggestion: Check if the referenced object '${objectName}' (source, query, view, field, dimension, measure) is defined correctly and spelled correctly (e.g., 'source: my_source is ...', 'query: my_query is ...'). Ensure it's defined *before* being used in the model file.`,
163
- );
164
- } else if (syntaxErrorMatch) {
165
- refined = true;
166
- suggestions.unshift(
167
- "Suggestion: There seems to be a syntax error near the location indicated in the error message. Check for misplaced/missing keywords (like 'is', '->', 'extend', 'where', 'aggregate', 'group_by', 'nest'), commas, brackets '{ }', or parentheses '( )'. See Query Syntax: https://docs.malloydata.dev/documentation/language/query",
168
- );
169
- } else if (connectionErrorMatch) {
170
- refined = true;
171
- suggestions.unshift(
172
- "Suggestion: Verify the database connection details (credentials, host, database name, connection name in Malloy) associated with this model.",
173
- );
174
- suggestions.push(
175
- "Ensure the database server is running and accessible from the publisher server.",
176
- );
177
- } else if (invalidRequestMatch) {
178
- refined = true;
179
- suggestions.unshift(
180
- "Suggestion: This error can occur when specifying a view using 'sourceName' and 'queryName'. Try providing the full query string directly in the 'query' parameter instead (e.g., 'query': 'run: your_source_name->your_view_name').",
181
- "Suggestion: Also verify you are providing either the 'query' parameter OR the correct combination of 'queryName' (and optionally 'sourceName' for views).",
182
- );
183
- }
184
-
185
- // If no specific pattern matched, keep the initial suggestions
186
- if (!refined) {
187
- suggestions = initialSuggestions;
188
- }
189
- } else {
190
- // Generic message if it's not a standard Error object
191
- baseMessage += `: ${String(error)}`;
192
- suggestions = initialSuggestions; // Use initial for non-errors too
193
- }
194
-
195
- return {
196
- message: baseMessage,
197
- suggestions: suggestions,
198
- };
199
- }