@malloy-publisher/server 0.0.165 → 0.0.168
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.
- package/.eslintrc.json +9 -1
- package/dist/app/api-doc.yaml +143 -1
- package/dist/app/assets/HomePage-D2tUw_9U.js +1 -0
- package/dist/app/assets/{MainPage-DAyUfYba.js → MainPage-DBQW76L7.js} +2 -2
- package/dist/app/assets/{ModelPage-CrMryV1s.js → ModelPage-BnfOKuhQ.js} +1 -1
- package/dist/app/assets/PackagePage-zPhE-rDg.js +1 -0
- package/dist/app/assets/ProjectPage-BpSTvuW6.js +1 -0
- package/dist/app/assets/RouteError-Cp9-yCK5.js +1 -0
- package/dist/app/assets/{WorkbookPage-DZEVYGW3.js → WorkbookPage-FD_gmxeE.js} +1 -1
- package/dist/app/assets/{index-BvVmB5sv.js → index-D5QBYuLK.js} +150 -150
- package/dist/app/assets/{index-CsC07BYd.js → index-DNCvL_5f.js} +1 -1
- package/dist/app/assets/{index-DWhjtyBB.js → index-x9S1fsYn.js} +1 -1
- package/dist/app/assets/{index.umd-DvM-lTQa.js → index.umd-CTYdFEHH.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.js +85955 -88560
- package/dist/server.js +197441 -106276
- package/package.json +2 -1
- package/src/controller/compile.controller.ts +35 -0
- package/src/controller/connection.controller.ts +22 -2
- package/src/controller/model.controller.ts +20 -9
- package/src/health.ts +8 -0
- package/src/instrumentation.ts +123 -34
- package/src/server.ts +49 -3
- package/src/service/connection.spec.ts +1331 -0
- package/src/service/connection.ts +407 -29
- package/src/service/db_utils.ts +104 -45
- package/src/service/gcs_s3_utils.ts +115 -40
- package/src/service/model.ts +5 -5
- package/src/service/project.ts +140 -4
- package/src/service/project_compile.spec.ts +197 -0
- package/src/service/project_store.ts +49 -21
- package/src/storage/StorageManager.ts +4 -3
- package/src/storage/duckdb/schema.ts +6 -5
- package/tests/harness/e2e.ts +4 -0
- package/tests/harness/mcp_test_setup.ts +172 -28
- package/tests/unit/duckdb/attached_databases.test.ts +61 -3
- package/tests/unit/ducklake/ducklake.test.ts +950 -0
- package/dist/app/assets/HomePage-QekMXs8r.js +0 -1
- package/dist/app/assets/PackagePage-DDaABD2A.js +0 -1
- package/dist/app/assets/ProjectPage-FAYUFGhL.js +0 -1
- package/dist/app/assets/RouteError-BKYctANX.js +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/server",
|
|
3
3
|
"description": "Malloy Publisher Server",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.168",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"malloy-publisher": "dist/server.js"
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.13.2",
|
|
43
43
|
"@opentelemetry/api": "^1.9.0",
|
|
44
44
|
"@opentelemetry/auto-instrumentations-node": "^0.57.0",
|
|
45
|
+
"@opentelemetry/exporter-prometheus": "^0.212.0",
|
|
45
46
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
46
47
|
"@opentelemetry/sdk-node": "^0.200.0",
|
|
47
48
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LogMessage } from "@malloydata/malloy";
|
|
2
|
+
import { ProjectStore } from "../service/project_store";
|
|
3
|
+
|
|
4
|
+
export class CompileController {
|
|
5
|
+
private projectStore: ProjectStore;
|
|
6
|
+
|
|
7
|
+
constructor(projectStore: ProjectStore) {
|
|
8
|
+
this.projectStore = projectStore;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public async compile(
|
|
12
|
+
projectName: string,
|
|
13
|
+
packageName: string,
|
|
14
|
+
modelName: string,
|
|
15
|
+
source: string,
|
|
16
|
+
includeSql: boolean = false,
|
|
17
|
+
): Promise<{ status: string; problems: LogMessage[]; sql?: string }> {
|
|
18
|
+
const project = await this.projectStore.getProject(projectName, false);
|
|
19
|
+
const { problems, sql } = await project.compileSource(
|
|
20
|
+
packageName,
|
|
21
|
+
modelName,
|
|
22
|
+
source,
|
|
23
|
+
includeSql,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Determine overall status based on presence of errors
|
|
27
|
+
const hasErrors = problems.some((p) => p.severity === "error");
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
status: hasErrors ? "error" : "success",
|
|
31
|
+
problems: problems,
|
|
32
|
+
...(sql !== undefined && { sql }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -155,17 +155,37 @@ export class ConnectionController {
|
|
|
155
155
|
public async getTable(
|
|
156
156
|
projectName: string,
|
|
157
157
|
connectionName: string,
|
|
158
|
-
|
|
158
|
+
schemaName: string,
|
|
159
159
|
tablePath: string,
|
|
160
160
|
): Promise<ApiTable> {
|
|
161
161
|
const malloyConnection = await this.getMalloyConnection(
|
|
162
162
|
projectName,
|
|
163
163
|
connectionName,
|
|
164
164
|
);
|
|
165
|
+
const connection = await this.getConnection(projectName, connectionName);
|
|
166
|
+
|
|
167
|
+
if (connection.type === "ducklake") {
|
|
168
|
+
if (tablePath.split(".").length === 1) {
|
|
169
|
+
// tablePath is just the table name, construct full path
|
|
170
|
+
tablePath = `${connectionName}.${schemaName}.${tablePath}`;
|
|
171
|
+
} else if (
|
|
172
|
+
tablePath.split(".").length === 2 &&
|
|
173
|
+
!tablePath.startsWith(connectionName)
|
|
174
|
+
) {
|
|
175
|
+
// tablePath is schemaName.tableName but missing connection prefix
|
|
176
|
+
tablePath = `${connectionName}.${tablePath}`;
|
|
177
|
+
}
|
|
178
|
+
// If tablePath already has 3+ parts or starts with connection name, use as-is
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const tableKey = tablePath.split(".").pop();
|
|
182
|
+
if (!tableKey) {
|
|
183
|
+
throw new Error(`Invalid tablePath: ${tablePath}`);
|
|
184
|
+
}
|
|
165
185
|
|
|
166
186
|
const tableSource = await getConnectionTableSource(
|
|
167
187
|
malloyConnection,
|
|
168
|
-
|
|
188
|
+
tableKey, // tableKey is the table name
|
|
169
189
|
tablePath,
|
|
170
190
|
);
|
|
171
191
|
|
|
@@ -38,16 +38,27 @@ export class ModelController {
|
|
|
38
38
|
packageName: string,
|
|
39
39
|
modelPath: string,
|
|
40
40
|
): Promise<ApiCompiledModel> {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
try {
|
|
42
|
+
const project = await this.projectStore.getProject(projectName, false);
|
|
43
|
+
const p = await project.getPackage(packageName, false);
|
|
44
|
+
const model = p.getModel(modelPath);
|
|
45
|
+
if (!model) {
|
|
46
|
+
throw new ModelNotFoundError(`${modelPath} does not exist`);
|
|
47
|
+
}
|
|
48
|
+
if (model.getType() === "notebook") {
|
|
49
|
+
throw new ModelNotFoundError(`${modelPath} is a notebook`);
|
|
50
|
+
}
|
|
51
|
+
return await model.getModel();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Re-throw ModelNotFoundError as-is
|
|
54
|
+
if (error instanceof ModelNotFoundError) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
// Wrap other errors with more context
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Failed to get model ${modelPath} from package ${packageName} in project ${projectName}: ${error}`,
|
|
60
|
+
);
|
|
49
61
|
}
|
|
50
|
-
return model.getModel();
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
public async getNotebook(
|
package/src/health.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { Express, NextFunction, Request, Response } from "express";
|
|
14
14
|
import { Server } from "http";
|
|
15
15
|
import { components } from "./api";
|
|
16
|
+
import { shutdownSDK } from "./instrumentation";
|
|
16
17
|
import { logger } from "./logger";
|
|
17
18
|
export type OperationalState =
|
|
18
19
|
components["schemas"]["ServerStatus"]["operationalState"];
|
|
@@ -112,6 +113,13 @@ export function registerSignalHandlers(
|
|
|
112
113
|
closeServer(mcpServer, "MCP server"),
|
|
113
114
|
]);
|
|
114
115
|
|
|
116
|
+
try {
|
|
117
|
+
await shutdownSDK();
|
|
118
|
+
logger.info("OpenTelemetry SDK shut down");
|
|
119
|
+
} catch (_error) {
|
|
120
|
+
/* do nothing */
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
try {
|
|
116
124
|
logger.close();
|
|
117
125
|
} catch (_error) {
|
package/src/instrumentation.ts
CHANGED
|
@@ -1,59 +1,148 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
3
|
-
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
4
|
-
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
1
|
+
import { metrics } from "@opentelemetry/api";
|
|
5
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";
|
|
6
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";
|
|
7
14
|
import {
|
|
8
15
|
ATTR_SERVICE_NAME,
|
|
9
16
|
ATTR_SERVICE_VERSION,
|
|
10
17
|
} from "@opentelemetry/semantic-conventions";
|
|
11
|
-
import {
|
|
12
|
-
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
13
|
-
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
18
|
+
import type { NextFunction, Request, Response } from "express";
|
|
14
19
|
import { logger } from "./logger";
|
|
15
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
|
+
|
|
16
44
|
function instrument() {
|
|
17
45
|
const otelCollectorUrl = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
18
|
-
if (!otelCollectorUrl) {
|
|
19
|
-
logger.info("No OTLP collector URL found, skipping instrumentation");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
26
|
-
const traceExporter = new OTLPTraceExporter({
|
|
27
|
-
url: `${otelCollectorUrl}/v1/traces`,
|
|
28
|
-
headers: {},
|
|
29
|
-
});
|
|
30
|
-
const metricExporter = new OTLPMetricExporter({
|
|
31
|
-
url: `${otelCollectorUrl}/v1/metrics`,
|
|
32
|
-
headers: {},
|
|
33
|
-
});
|
|
34
|
-
const logExporter = new OTLPLogExporter({
|
|
35
|
-
url: `${otelCollectorUrl}/v1/logs`,
|
|
36
|
-
headers: {},
|
|
47
|
+
prometheusExporter = new PrometheusExporter({
|
|
48
|
+
preventServerStart: true,
|
|
37
49
|
});
|
|
38
50
|
|
|
39
|
-
const
|
|
40
|
-
|
|
51
|
+
const instrumentations = [
|
|
52
|
+
getNodeAutoInstrumentations(),
|
|
53
|
+
new ExpressInstrumentation({
|
|
54
|
+
ignoreLayersType: [ExpressLayerType.MIDDLEWARE],
|
|
55
|
+
ignoreLayers: [/\/health/, /\/metrics/],
|
|
56
|
+
}),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
sdk = new NodeSDK({
|
|
41
60
|
resource: resourceFromAttributes({
|
|
42
61
|
[ATTR_SERVICE_NAME]: "publisher",
|
|
43
62
|
[ATTR_SERVICE_VERSION]: "1.0.0",
|
|
44
63
|
}),
|
|
45
64
|
autoDetectResources: true,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
],
|
|
49
82
|
}),
|
|
50
|
-
instrumentations: [getNodeAutoInstrumentations()],
|
|
51
|
-
spanProcessors: [new BatchSpanProcessor(traceExporter)],
|
|
52
|
-
logRecordProcessors: [new BatchLogRecordProcessor(logExporter)],
|
|
53
83
|
});
|
|
54
84
|
|
|
55
85
|
sdk.start();
|
|
56
|
-
|
|
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
|
+
}
|
|
57
94
|
}
|
|
58
95
|
|
|
59
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/server.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Pre-load the instrumentation module; the instrumentation module must be loaded before the other imports.
|
|
2
|
+
import "./instrumentation";
|
|
3
|
+
import {
|
|
4
|
+
getPrometheusMetricsHandler,
|
|
5
|
+
httpMetricsMiddleware,
|
|
6
|
+
} from "./instrumentation";
|
|
7
|
+
|
|
1
8
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
9
|
import * as bodyParser from "body-parser";
|
|
3
10
|
import cors from "cors";
|
|
@@ -6,6 +13,7 @@ import * as http from "http";
|
|
|
6
13
|
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
7
14
|
import { AddressInfo } from "net";
|
|
8
15
|
import * as path from "path";
|
|
16
|
+
import { CompileController } from "./controller/compile.controller";
|
|
9
17
|
import { ConnectionController } from "./controller/connection.controller";
|
|
10
18
|
import { DatabaseController } from "./controller/database.controller";
|
|
11
19
|
import { ModelController } from "./controller/model.controller";
|
|
@@ -19,6 +27,7 @@ import {
|
|
|
19
27
|
registerSignalHandlers,
|
|
20
28
|
} from "./health";
|
|
21
29
|
import { logger, loggerMiddleware } from "./logger";
|
|
30
|
+
|
|
22
31
|
import { initializeMcpServer } from "./mcp/server";
|
|
23
32
|
import { ProjectStore } from "./service/project_store";
|
|
24
33
|
// Parse command line arguments
|
|
@@ -110,7 +119,7 @@ const isDevelopment = process.env["NODE_ENV"] === "development";
|
|
|
110
119
|
|
|
111
120
|
const app = express();
|
|
112
121
|
app.use(loggerMiddleware);
|
|
113
|
-
|
|
122
|
+
app.use(httpMetricsMiddleware);
|
|
114
123
|
const projectStore = new ProjectStore(SERVER_ROOT);
|
|
115
124
|
const watchModeController = new WatchModeController(projectStore);
|
|
116
125
|
const connectionController = new ConnectionController(projectStore);
|
|
@@ -118,9 +127,13 @@ const modelController = new ModelController(projectStore);
|
|
|
118
127
|
const packageController = new PackageController(projectStore);
|
|
119
128
|
const databaseController = new DatabaseController(projectStore);
|
|
120
129
|
const queryController = new QueryController(projectStore);
|
|
130
|
+
const compileController = new CompileController(projectStore);
|
|
121
131
|
|
|
122
132
|
export const mcpApp = express();
|
|
123
133
|
|
|
134
|
+
// Register health endpoints on mcpApp (for E2E tests)
|
|
135
|
+
registerHealthEndpoints(mcpApp);
|
|
136
|
+
|
|
124
137
|
mcpApp.use(MCP_ENDPOINT, express.json());
|
|
125
138
|
mcpApp.use(MCP_ENDPOINT, cors());
|
|
126
139
|
|
|
@@ -220,7 +233,10 @@ if (!isDevelopment) {
|
|
|
220
233
|
target: "http://localhost:5173",
|
|
221
234
|
changeOrigin: true,
|
|
222
235
|
ws: true,
|
|
223
|
-
pathFilter: (path) =>
|
|
236
|
+
pathFilter: (path) =>
|
|
237
|
+
!path.startsWith("/api/") &&
|
|
238
|
+
!path.startsWith("/metrics") &&
|
|
239
|
+
!path.startsWith("/health"),
|
|
224
240
|
}),
|
|
225
241
|
);
|
|
226
242
|
}
|
|
@@ -240,9 +256,19 @@ app.use(
|
|
|
240
256
|
);
|
|
241
257
|
app.use(bodyParser.json());
|
|
242
258
|
|
|
243
|
-
// Register health check endpoints
|
|
259
|
+
// Register health check endpoints on main app:
|
|
260
|
+
// - Required for production/Kubernetes monitoring (main server on PUBLISHER_PORT)
|
|
244
261
|
registerHealthEndpoints(app);
|
|
245
262
|
|
|
263
|
+
// Register Prometheus metrics endpoint
|
|
264
|
+
try {
|
|
265
|
+
const metricsHandler = getPrometheusMetricsHandler();
|
|
266
|
+
app.get("/metrics", metricsHandler);
|
|
267
|
+
logger.info("Prometheus metrics endpoint registered at /metrics");
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logger.warn("Failed to register Prometheus metrics endpoint", { error });
|
|
270
|
+
}
|
|
271
|
+
|
|
246
272
|
// Register draining guard middleware - must be after health endpoints but before other routes
|
|
247
273
|
app.use(drainingGuard);
|
|
248
274
|
|
|
@@ -897,6 +923,26 @@ app.get(
|
|
|
897
923
|
},
|
|
898
924
|
);
|
|
899
925
|
|
|
926
|
+
app.post(
|
|
927
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/models/:modelName/compile`,
|
|
928
|
+
async (req, res) => {
|
|
929
|
+
try {
|
|
930
|
+
const result = await compileController.compile(
|
|
931
|
+
req.params.projectName,
|
|
932
|
+
req.params.packageName,
|
|
933
|
+
req.params.modelName,
|
|
934
|
+
req.body.source,
|
|
935
|
+
req.body.includeSql === true,
|
|
936
|
+
);
|
|
937
|
+
res.status(200).json(result);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
logger.error("Compilation error", { error });
|
|
940
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
941
|
+
res.status(status).json(json);
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
);
|
|
945
|
+
|
|
900
946
|
// Modify the catch-all route to only serve index.html in production
|
|
901
947
|
if (!isDevelopment) {
|
|
902
948
|
app.get("*", (_req, res) => res.sendFile(path.resolve(ROOT, "index.html")));
|