@malloy-publisher/server 0.0.157 → 0.0.159
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/dist/instrumentation.js +26 -2
- package/dist/server.js +138 -58
- package/k6-tests/README.md +88 -0
- package/k6-tests/breakpoint-test.ts +5 -0
- package/k6-tests/bun.lock +797 -0
- package/k6-tests/client_factory.ts +128 -0
- package/k6-tests/common.ts +328 -82
- package/k6-tests/package.json +30 -0
- package/k6-tests/tsconfig.json +29 -0
- package/k6-tests/types.d.ts +14 -5
- package/package.json +1 -1
- package/src/controller/package.controller.ts +1 -1
- package/src/logger.ts +44 -2
- package/src/mcp/server.ts +2 -2
- package/src/server.ts +0 -2
- package/src/service/model.spec.ts +8 -0
- package/src/service/model.ts +63 -9
- package/src/service/package.ts +20 -11
- package/src/service/project.ts +26 -2
- package/src/service/project_store.ts +7 -2
- package/src/storage/duckdb/DuckDBConnection.ts +54 -46
package/dist/instrumentation.js
CHANGED
|
@@ -116693,7 +116693,20 @@ var import_sdk_logs = __toESM(require_src7());
|
|
|
116693
116693
|
// src/logger.ts
|
|
116694
116694
|
var import_winston = __toESM(require_winston());
|
|
116695
116695
|
var isTelemetryEnabled = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
|
|
116696
|
+
var VALID_LOG_LEVELS = ["error", "warn", "info", "verbose", "debug", "silly"];
|
|
116697
|
+
var getLogLevel = () => {
|
|
116698
|
+
if (process.env.LOG_LEVEL) {
|
|
116699
|
+
const logLevel = process.env.LOG_LEVEL.toLowerCase();
|
|
116700
|
+
if (VALID_LOG_LEVELS.includes(logLevel)) {
|
|
116701
|
+
return logLevel;
|
|
116702
|
+
} else {
|
|
116703
|
+
console.error(`Invalid log level: ${process.env.LOG_LEVEL}. Valid log levels are: ${VALID_LOG_LEVELS.join(", ")}. Defaulting to "debug".`);
|
|
116704
|
+
}
|
|
116705
|
+
}
|
|
116706
|
+
return "debug";
|
|
116707
|
+
};
|
|
116696
116708
|
var logger = import_winston.default.createLogger({
|
|
116709
|
+
level: getLogLevel(),
|
|
116697
116710
|
format: isTelemetryEnabled ? import_winston.default.format.combine(import_winston.default.format.uncolorize(), import_winston.default.format.timestamp(), import_winston.default.format.metadata({
|
|
116698
116711
|
fillExcept: ["message", "level", "timestamp"]
|
|
116699
116712
|
}), import_winston.default.format.json()) : import_winston.default.format.combine(import_winston.default.format.colorize(), import_winston.default.format.simple()),
|
|
@@ -116710,6 +116723,14 @@ function extractTraceIdFromTraceparent(traceparent) {
|
|
|
116710
116723
|
}
|
|
116711
116724
|
return;
|
|
116712
116725
|
}
|
|
116726
|
+
var DISABLE_RESPONSE_LOGGING = process.env.DISABLE_RESPONSE_LOGGING === "true" || process.env.DISABLE_RESPONSE_LOGGING === "1";
|
|
116727
|
+
function formatDuration(durationMs) {
|
|
116728
|
+
if (durationMs >= 1000) {
|
|
116729
|
+
const seconds = durationMs / 1000;
|
|
116730
|
+
return `${seconds.toFixed(2)}s`;
|
|
116731
|
+
}
|
|
116732
|
+
return `${durationMs.toFixed(2)}ms`;
|
|
116733
|
+
}
|
|
116713
116734
|
var loggerMiddleware = (req, res, next) => {
|
|
116714
116735
|
const startTime = performance.now();
|
|
116715
116736
|
const resJson = res.json;
|
|
@@ -116719,16 +116740,19 @@ var loggerMiddleware = (req, res, next) => {
|
|
|
116719
116740
|
};
|
|
116720
116741
|
res.on("finish", () => {
|
|
116721
116742
|
const endTime = performance.now();
|
|
116743
|
+
const durationMs = endTime - startTime;
|
|
116722
116744
|
const traceparent = req.headers["traceparent"];
|
|
116723
116745
|
const traceId = extractTraceIdFromTraceparent(traceparent);
|
|
116724
116746
|
const logMetadata = {
|
|
116725
116747
|
statusCode: res.statusCode,
|
|
116726
|
-
duration:
|
|
116748
|
+
duration: formatDuration(durationMs),
|
|
116727
116749
|
payload: req.body,
|
|
116728
|
-
response: res.locals.body,
|
|
116729
116750
|
params: req.params,
|
|
116730
116751
|
query: req.query
|
|
116731
116752
|
};
|
|
116753
|
+
if (!DISABLE_RESPONSE_LOGGING) {
|
|
116754
|
+
logMetadata.response = res.locals.body;
|
|
116755
|
+
}
|
|
116732
116756
|
if (traceId) {
|
|
116733
116757
|
logMetadata.traceId = traceId;
|
|
116734
116758
|
}
|
package/dist/server.js
CHANGED
|
@@ -125120,7 +125120,20 @@ class FrozenConfigError extends Error {
|
|
|
125120
125120
|
// src/logger.ts
|
|
125121
125121
|
var import_winston = __toESM(require_winston());
|
|
125122
125122
|
var isTelemetryEnabled = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
|
|
125123
|
+
var VALID_LOG_LEVELS = ["error", "warn", "info", "verbose", "debug", "silly"];
|
|
125124
|
+
var getLogLevel = () => {
|
|
125125
|
+
if (process.env.LOG_LEVEL) {
|
|
125126
|
+
const logLevel = process.env.LOG_LEVEL.toLowerCase();
|
|
125127
|
+
if (VALID_LOG_LEVELS.includes(logLevel)) {
|
|
125128
|
+
return logLevel;
|
|
125129
|
+
} else {
|
|
125130
|
+
console.error(`Invalid log level: ${process.env.LOG_LEVEL}. Valid log levels are: ${VALID_LOG_LEVELS.join(", ")}. Defaulting to "debug".`);
|
|
125131
|
+
}
|
|
125132
|
+
}
|
|
125133
|
+
return "debug";
|
|
125134
|
+
};
|
|
125123
125135
|
var logger = import_winston.default.createLogger({
|
|
125136
|
+
level: getLogLevel(),
|
|
125124
125137
|
format: isTelemetryEnabled ? import_winston.default.format.combine(import_winston.default.format.uncolorize(), import_winston.default.format.timestamp(), import_winston.default.format.metadata({
|
|
125125
125138
|
fillExcept: ["message", "level", "timestamp"]
|
|
125126
125139
|
}), import_winston.default.format.json()) : import_winston.default.format.combine(import_winston.default.format.colorize(), import_winston.default.format.simple()),
|
|
@@ -125137,6 +125150,14 @@ function extractTraceIdFromTraceparent(traceparent) {
|
|
|
125137
125150
|
}
|
|
125138
125151
|
return;
|
|
125139
125152
|
}
|
|
125153
|
+
var DISABLE_RESPONSE_LOGGING = process.env.DISABLE_RESPONSE_LOGGING === "true" || process.env.DISABLE_RESPONSE_LOGGING === "1";
|
|
125154
|
+
function formatDuration(durationMs) {
|
|
125155
|
+
if (durationMs >= 1000) {
|
|
125156
|
+
const seconds = durationMs / 1000;
|
|
125157
|
+
return `${seconds.toFixed(2)}s`;
|
|
125158
|
+
}
|
|
125159
|
+
return `${durationMs.toFixed(2)}ms`;
|
|
125160
|
+
}
|
|
125140
125161
|
var loggerMiddleware = (req, res, next) => {
|
|
125141
125162
|
const startTime = performance.now();
|
|
125142
125163
|
const resJson = res.json;
|
|
@@ -125146,16 +125167,19 @@ var loggerMiddleware = (req, res, next) => {
|
|
|
125146
125167
|
};
|
|
125147
125168
|
res.on("finish", () => {
|
|
125148
125169
|
const endTime = performance.now();
|
|
125170
|
+
const durationMs = endTime - startTime;
|
|
125149
125171
|
const traceparent = req.headers["traceparent"];
|
|
125150
125172
|
const traceId = extractTraceIdFromTraceparent(traceparent);
|
|
125151
125173
|
const logMetadata = {
|
|
125152
125174
|
statusCode: res.statusCode,
|
|
125153
|
-
duration:
|
|
125175
|
+
duration: formatDuration(durationMs),
|
|
125154
125176
|
payload: req.body,
|
|
125155
|
-
response: res.locals.body,
|
|
125156
125177
|
params: req.params,
|
|
125157
125178
|
query: req.query
|
|
125158
125179
|
};
|
|
125180
|
+
if (!DISABLE_RESPONSE_LOGGING) {
|
|
125181
|
+
logMetadata.response = res.locals.body;
|
|
125182
|
+
}
|
|
125159
125183
|
if (traceId) {
|
|
125160
125184
|
logMetadata.traceId = traceId;
|
|
125161
125185
|
}
|
|
@@ -130122,7 +130146,6 @@ class ModelController {
|
|
|
130122
130146
|
|
|
130123
130147
|
// src/controller/package.controller.ts
|
|
130124
130148
|
var path2 = __toESM(require("path"));
|
|
130125
|
-
|
|
130126
130149
|
class PackageController {
|
|
130127
130150
|
projectStore;
|
|
130128
130151
|
constructor(projectStore) {
|
|
@@ -136043,6 +136066,7 @@ class DuckDBConnection2 {
|
|
|
136043
136066
|
db = null;
|
|
136044
136067
|
connection = null;
|
|
136045
136068
|
dbPath;
|
|
136069
|
+
mutex = new Mutex;
|
|
136046
136070
|
constructor(dbPath) {
|
|
136047
136071
|
this.dbPath = dbPath || path4.join(process.cwd(), "publisher.db");
|
|
136048
136072
|
}
|
|
@@ -136099,13 +136123,15 @@ class DuckDBConnection2 {
|
|
|
136099
136123
|
async isInitialized() {
|
|
136100
136124
|
if (!this.connection)
|
|
136101
136125
|
return false;
|
|
136102
|
-
return
|
|
136103
|
-
|
|
136104
|
-
|
|
136105
|
-
|
|
136106
|
-
|
|
136107
|
-
|
|
136108
|
-
|
|
136126
|
+
return this.mutex.runExclusive(async () => {
|
|
136127
|
+
return new Promise((resolve3) => {
|
|
136128
|
+
this.connection.all("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'", (err, rows) => {
|
|
136129
|
+
if (err) {
|
|
136130
|
+
resolve3(false);
|
|
136131
|
+
return;
|
|
136132
|
+
}
|
|
136133
|
+
resolve3(rows && rows.length > 0);
|
|
136134
|
+
});
|
|
136109
136135
|
});
|
|
136110
136136
|
});
|
|
136111
136137
|
}
|
|
@@ -136113,40 +136139,44 @@ class DuckDBConnection2 {
|
|
|
136113
136139
|
if (!this.connection) {
|
|
136114
136140
|
throw new Error("Database not initialized");
|
|
136115
136141
|
}
|
|
136116
|
-
return
|
|
136117
|
-
|
|
136118
|
-
|
|
136119
|
-
|
|
136142
|
+
return this.mutex.runExclusive(async () => {
|
|
136143
|
+
return new Promise((resolve3, reject) => {
|
|
136144
|
+
const callback = (err) => {
|
|
136145
|
+
if (err) {
|
|
136146
|
+
reject(new Error(`Query execution failed: ${err.message}
|
|
136120
136147
|
Query: ${query}`));
|
|
136121
|
-
|
|
136148
|
+
return;
|
|
136149
|
+
}
|
|
136150
|
+
resolve3();
|
|
136151
|
+
};
|
|
136152
|
+
if (params && params.length > 0) {
|
|
136153
|
+
this.connection.run(query, ...params, callback);
|
|
136154
|
+
} else {
|
|
136155
|
+
this.connection.run(query, callback);
|
|
136122
136156
|
}
|
|
136123
|
-
|
|
136124
|
-
};
|
|
136125
|
-
if (params && params.length > 0) {
|
|
136126
|
-
this.connection.run(query, ...params, callback);
|
|
136127
|
-
} else {
|
|
136128
|
-
this.connection.run(query, callback);
|
|
136129
|
-
}
|
|
136157
|
+
});
|
|
136130
136158
|
});
|
|
136131
136159
|
}
|
|
136132
136160
|
async all(query, params) {
|
|
136133
136161
|
if (!this.connection) {
|
|
136134
136162
|
throw new Error("Database not initialized");
|
|
136135
136163
|
}
|
|
136136
|
-
return
|
|
136137
|
-
|
|
136138
|
-
|
|
136139
|
-
|
|
136164
|
+
return this.mutex.runExclusive(async () => {
|
|
136165
|
+
return new Promise((resolve3, reject) => {
|
|
136166
|
+
const callback = (err, rows) => {
|
|
136167
|
+
if (err) {
|
|
136168
|
+
reject(new Error(`Query execution failed: ${err.message}
|
|
136140
136169
|
Query: ${query}`));
|
|
136141
|
-
|
|
136170
|
+
return;
|
|
136171
|
+
}
|
|
136172
|
+
resolve3(rows || []);
|
|
136173
|
+
};
|
|
136174
|
+
if (params && params.length > 0) {
|
|
136175
|
+
this.connection.all(query, ...params, callback);
|
|
136176
|
+
} else {
|
|
136177
|
+
this.connection.all(query, callback);
|
|
136142
136178
|
}
|
|
136143
|
-
|
|
136144
|
-
};
|
|
136145
|
-
if (params && params.length > 0) {
|
|
136146
|
-
this.connection.all(query, ...params, callback);
|
|
136147
|
-
} else {
|
|
136148
|
-
this.connection.all(query, callback);
|
|
136149
|
-
}
|
|
136179
|
+
});
|
|
136150
136180
|
});
|
|
136151
136181
|
}
|
|
136152
136182
|
async get(query, params) {
|
|
@@ -136750,6 +136780,7 @@ class Model {
|
|
|
136750
136780
|
modelInfo;
|
|
136751
136781
|
sources;
|
|
136752
136782
|
queries;
|
|
136783
|
+
sourceInfos;
|
|
136753
136784
|
runnableNotebookCells;
|
|
136754
136785
|
compilationError;
|
|
136755
136786
|
meter = import_api.metrics.getMeter("publisher");
|
|
@@ -136757,7 +136788,7 @@ class Model {
|
|
|
136757
136788
|
description: "How long it takes to execute a Malloy model query",
|
|
136758
136789
|
unit: "ms"
|
|
136759
136790
|
});
|
|
136760
|
-
constructor(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, runnableNotebookCells, compilationError) {
|
|
136791
|
+
constructor(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos, runnableNotebookCells, compilationError) {
|
|
136761
136792
|
this.packageName = packageName;
|
|
136762
136793
|
this.modelPath = modelPath;
|
|
136763
136794
|
this.dataStyles = dataStyles;
|
|
@@ -136766,6 +136797,7 @@ class Model {
|
|
|
136766
136797
|
this.modelMaterializer = modelMaterializer;
|
|
136767
136798
|
this.sources = sources;
|
|
136768
136799
|
this.queries = queries;
|
|
136800
|
+
this.sourceInfos = sourceInfos;
|
|
136769
136801
|
this.runnableNotebookCells = runnableNotebookCells;
|
|
136770
136802
|
this.compilationError = compilationError;
|
|
136771
136803
|
this.modelInfo = this.modelDef ? import_malloy2.modelDefToModelInfo(this.modelDef) : undefined;
|
|
@@ -136777,12 +136809,41 @@ class Model {
|
|
|
136777
136809
|
let modelDef = undefined;
|
|
136778
136810
|
let sources = undefined;
|
|
136779
136811
|
let queries = undefined;
|
|
136812
|
+
const sourceInfos = [];
|
|
136780
136813
|
if (modelMaterializer) {
|
|
136781
136814
|
modelDef = (await modelMaterializer.getModel())._modelDef;
|
|
136782
136815
|
sources = Model.getSources(modelPath, modelDef);
|
|
136783
136816
|
queries = Model.getQueries(modelPath, modelDef);
|
|
136817
|
+
const imports = modelDef.imports || [];
|
|
136818
|
+
const importedSourceNames = new Set;
|
|
136819
|
+
for (const importLocation of imports) {
|
|
136820
|
+
try {
|
|
136821
|
+
const modelString = await runtime.urlReader.readURL(new URL(importLocation.importURL));
|
|
136822
|
+
const importedModelDef = (await runtime.loadModel(modelString, { importBaseURL }).getModel())._modelDef;
|
|
136823
|
+
const importedModelInfo = import_malloy2.modelDefToModelInfo(importedModelDef);
|
|
136824
|
+
const importedSources = importedModelInfo.entries.filter((entry) => entry.kind === "source");
|
|
136825
|
+
for (const source of importedSources) {
|
|
136826
|
+
if (!importedSourceNames.has(source.name)) {
|
|
136827
|
+
sourceInfos.push(source);
|
|
136828
|
+
importedSourceNames.add(source.name);
|
|
136829
|
+
}
|
|
136830
|
+
}
|
|
136831
|
+
} catch (importError) {
|
|
136832
|
+
logger.warn("Failed to load sourceInfo from import", {
|
|
136833
|
+
importURL: importLocation.importURL,
|
|
136834
|
+
error: importError
|
|
136835
|
+
});
|
|
136836
|
+
}
|
|
136837
|
+
}
|
|
136838
|
+
const localModelInfo = import_malloy2.modelDefToModelInfo(modelDef);
|
|
136839
|
+
const localSources = localModelInfo.entries.filter((entry) => entry.kind === "source");
|
|
136840
|
+
for (const source of localSources) {
|
|
136841
|
+
if (!importedSourceNames.has(source.name)) {
|
|
136842
|
+
sourceInfos.push(source);
|
|
136843
|
+
}
|
|
136844
|
+
}
|
|
136784
136845
|
}
|
|
136785
|
-
return new Model(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, runnableNotebookCells, undefined);
|
|
136846
|
+
return new Model(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos.length > 0 ? sourceInfos : undefined, runnableNotebookCells, undefined);
|
|
136786
136847
|
} catch (error) {
|
|
136787
136848
|
let computedError = error;
|
|
136788
136849
|
if (error instanceof Error && error.stack) {
|
|
@@ -136795,7 +136856,7 @@ class Model {
|
|
|
136795
136856
|
}
|
|
136796
136857
|
computedError = new ModelCompilationError(error);
|
|
136797
136858
|
}
|
|
136798
|
-
return new Model(packageName, modelPath, dataStyles, modelType, undefined, undefined, undefined, undefined, undefined, computedError);
|
|
136859
|
+
return new Model(packageName, modelPath, dataStyles, modelType, undefined, undefined, undefined, undefined, undefined, undefined, computedError);
|
|
136799
136860
|
}
|
|
136800
136861
|
}
|
|
136801
136862
|
getPath() {
|
|
@@ -136808,9 +136869,7 @@ class Model {
|
|
|
136808
136869
|
return this.sources;
|
|
136809
136870
|
}
|
|
136810
136871
|
getSourceInfos() {
|
|
136811
|
-
return this.
|
|
136812
|
-
return entry.kind === "source";
|
|
136813
|
-
}) : undefined;
|
|
136872
|
+
return this.sourceInfos;
|
|
136814
136873
|
}
|
|
136815
136874
|
getQueries() {
|
|
136816
136875
|
return this.sources;
|
|
@@ -137035,8 +137094,9 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`);
|
|
|
137035
137094
|
const modelConnections = new Map;
|
|
137036
137095
|
for (const [name, connection] of connections.entries()) {
|
|
137037
137096
|
const isDuckDB = connection instanceof import_db_duckdb2.DuckDBConnection || connection.constructor.name === "DuckDBConnection";
|
|
137038
|
-
if (isDuckDB) {
|
|
137039
|
-
const
|
|
137097
|
+
if (isDuckDB && connection.workingDirectory !== workingDirectory) {
|
|
137098
|
+
const databasePath = connection.databasePath || ":memory:";
|
|
137099
|
+
const modelDuckDBConnection = new import_db_duckdb2.DuckDBConnection(name, databasePath, workingDirectory);
|
|
137040
137100
|
modelConnections.set(name, modelDuckDBConnection);
|
|
137041
137101
|
} else {
|
|
137042
137102
|
modelConnections.set(name, connection);
|
|
@@ -137226,16 +137286,14 @@ class Package {
|
|
|
137226
137286
|
const manifestValidationTime = performance.now();
|
|
137227
137287
|
logger.info("Package manifest validation completed", {
|
|
137228
137288
|
packageName,
|
|
137229
|
-
duration: manifestValidationTime - startTime
|
|
137230
|
-
unit: "ms"
|
|
137289
|
+
duration: formatDuration(manifestValidationTime - startTime)
|
|
137231
137290
|
});
|
|
137232
137291
|
try {
|
|
137233
137292
|
const packageConfig = await Package.readPackageConfig(packagePath);
|
|
137234
137293
|
const packageConfigTime = performance.now();
|
|
137235
137294
|
logger.info("Package config read completed", {
|
|
137236
137295
|
packageName,
|
|
137237
|
-
duration: packageConfigTime - manifestValidationTime
|
|
137238
|
-
unit: "ms"
|
|
137296
|
+
duration: formatDuration(packageConfigTime - manifestValidationTime)
|
|
137239
137297
|
});
|
|
137240
137298
|
packageConfig.resource = `${API_PREFIX}/projects/${projectName}/packages/${packageName}`;
|
|
137241
137299
|
const databases = await Package.readDatabases(packagePath);
|
|
@@ -137243,8 +137301,7 @@ class Package {
|
|
|
137243
137301
|
logger.info("Databases read completed", {
|
|
137244
137302
|
packageName,
|
|
137245
137303
|
databaseCount: databases.length,
|
|
137246
|
-
duration: databasesTime - packageConfigTime
|
|
137247
|
-
unit: "ms"
|
|
137304
|
+
duration: formatDuration(databasesTime - packageConfigTime)
|
|
137248
137305
|
});
|
|
137249
137306
|
const connections = new Map(projectConnections);
|
|
137250
137307
|
const duckdbConnections = await createPackageDuckDBConnections(packageConnections, packagePath);
|
|
@@ -137256,8 +137313,7 @@ class Package {
|
|
|
137256
137313
|
logger.info("Models loaded", {
|
|
137257
137314
|
packageName,
|
|
137258
137315
|
modelCount: models.size,
|
|
137259
|
-
duration: modelsTime - databasesTime
|
|
137260
|
-
unit: "ms"
|
|
137316
|
+
duration: formatDuration(modelsTime - databasesTime)
|
|
137261
137317
|
});
|
|
137262
137318
|
for (const [modelPath, model] of models.entries()) {
|
|
137263
137319
|
const maybeModel = model;
|
|
@@ -137284,8 +137340,7 @@ class Package {
|
|
|
137284
137340
|
});
|
|
137285
137341
|
logger.info(`Successfully loaded package ${packageName}`, {
|
|
137286
137342
|
packageName,
|
|
137287
|
-
duration: executionTime
|
|
137288
|
-
unit: "ms"
|
|
137343
|
+
duration: formatDuration(executionTime)
|
|
137289
137344
|
});
|
|
137290
137345
|
return new Package(projectName, packageName, packagePath, packageConfig, databases, models, connections);
|
|
137291
137346
|
} catch (error) {
|
|
@@ -137297,6 +137352,17 @@ class Package {
|
|
|
137297
137352
|
malloy_package_name: packageName,
|
|
137298
137353
|
status: "error"
|
|
137299
137354
|
});
|
|
137355
|
+
try {
|
|
137356
|
+
await fs5.rm(packagePath, {
|
|
137357
|
+
recursive: true,
|
|
137358
|
+
force: true
|
|
137359
|
+
});
|
|
137360
|
+
logger.info(`Cleaned up failed package directory: ${packagePath}`);
|
|
137361
|
+
} catch (cleanupError) {
|
|
137362
|
+
logger.warn(`Failed to clean up package directory ${packagePath}`, {
|
|
137363
|
+
error: cleanupError
|
|
137364
|
+
});
|
|
137365
|
+
}
|
|
137300
137366
|
throw error;
|
|
137301
137367
|
}
|
|
137302
137368
|
}
|
|
@@ -137708,17 +137774,31 @@ class Project {
|
|
|
137708
137774
|
this.apiConnections = apiConnections;
|
|
137709
137775
|
}
|
|
137710
137776
|
deleteConnection(connectionName) {
|
|
137711
|
-
|
|
137777
|
+
this.malloyConnections.get(connectionName)?.close();
|
|
137778
|
+
const isDeleted = this.malloyConnections.delete(connectionName);
|
|
137712
137779
|
const index = this.apiConnections.findIndex((conn) => conn.name === connectionName);
|
|
137713
137780
|
if (index !== -1) {
|
|
137714
137781
|
this.apiConnections.splice(index, 1);
|
|
137715
137782
|
}
|
|
137716
|
-
if (
|
|
137783
|
+
if (isDeleted || index !== -1) {
|
|
137717
137784
|
logger.info(`Removed connection ${connectionName} from project ${this.projectName}`);
|
|
137718
137785
|
} else {
|
|
137719
137786
|
logger.warn(`Connection ${connectionName} not found in project ${this.projectName}`);
|
|
137720
137787
|
}
|
|
137721
137788
|
}
|
|
137789
|
+
closeAllConnections() {
|
|
137790
|
+
for (const [connectionName, connection] of this.malloyConnections) {
|
|
137791
|
+
try {
|
|
137792
|
+
connection.close();
|
|
137793
|
+
logger.info(`Closed connection ${connectionName} for project ${this.projectName}`);
|
|
137794
|
+
} catch (error) {
|
|
137795
|
+
logger.error(`Error closing connection ${connectionName} for project ${this.projectName}`, { error });
|
|
137796
|
+
}
|
|
137797
|
+
}
|
|
137798
|
+
this.malloyConnections.clear();
|
|
137799
|
+
this.apiConnections = [];
|
|
137800
|
+
logger.info(`Closed all connections for project ${this.projectName}`);
|
|
137801
|
+
}
|
|
137722
137802
|
async serialize() {
|
|
137723
137803
|
return {
|
|
137724
137804
|
...this.metadata,
|
|
@@ -137820,7 +137900,8 @@ class ProjectStore {
|
|
|
137820
137900
|
}
|
|
137821
137901
|
}
|
|
137822
137902
|
this.isInitialized = true;
|
|
137823
|
-
|
|
137903
|
+
const initializationDuration = performance.now() - initialTime;
|
|
137904
|
+
logger.info(`Project store successfully initialized in ${formatDuration(initializationDuration)}`);
|
|
137824
137905
|
} catch (error) {
|
|
137825
137906
|
logger.error("Error initializing project store", { error });
|
|
137826
137907
|
process.exit(1);
|
|
@@ -138192,6 +138273,7 @@ class ProjectStore {
|
|
|
138192
138273
|
return;
|
|
138193
138274
|
}
|
|
138194
138275
|
const projectPath = project.metadata?.location;
|
|
138276
|
+
project.closeAllConnections();
|
|
138195
138277
|
this.projects.delete(projectName);
|
|
138196
138278
|
await this.deleteProjectFromDatabase(projectName);
|
|
138197
138279
|
if (projectPath) {
|
|
@@ -142492,7 +142574,7 @@ function initializeMcpServer(projectStore) {
|
|
|
142492
142574
|
registerPromptCapability(mcpServer, projectStore);
|
|
142493
142575
|
const endTime = performance.now();
|
|
142494
142576
|
logger.info(`[MCP Init] Finished initializeMcpServer`, {
|
|
142495
|
-
duration: endTime - startTime
|
|
142577
|
+
duration: formatDuration(endTime - startTime)
|
|
142496
142578
|
});
|
|
142497
142579
|
return mcpServer;
|
|
142498
142580
|
}
|
|
@@ -142770,7 +142852,6 @@ app.get(`${API_PREFIX2}/projects/:projectName/connections/:connectionName/schema
|
|
|
142770
142852
|
logger.info("req.params", { params: req.params });
|
|
142771
142853
|
try {
|
|
142772
142854
|
const results = await connectionController.listTables(req.params.projectName, req.params.connectionName, req.params.schemaName);
|
|
142773
|
-
logger.info("results", { results });
|
|
142774
142855
|
res.status(200).json(results);
|
|
142775
142856
|
} catch (error) {
|
|
142776
142857
|
logger.error(error);
|
|
@@ -142782,7 +142863,6 @@ app.get(`${API_PREFIX2}/projects/:projectName/connections/:connectionName/schema
|
|
|
142782
142863
|
logger.info("req.params", { params: req.params });
|
|
142783
142864
|
try {
|
|
142784
142865
|
const results = await connectionController.getTable(req.params.projectName, req.params.connectionName, req.params.schemaName, req.params.tablePath);
|
|
142785
|
-
logger.info("results", { results });
|
|
142786
142866
|
res.status(200).json(results);
|
|
142787
142867
|
} catch (error) {
|
|
142788
142868
|
logger.error(error);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# k6-tests
|
|
2
|
+
|
|
3
|
+
Performance and load testing suite for the Malloy Publisher API using k6.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To install dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Available Tests
|
|
14
|
+
|
|
15
|
+
### Smoke Test
|
|
16
|
+
|
|
17
|
+
Basic functionality verification under minimal load:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun run smoke-test
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Load Tests
|
|
24
|
+
|
|
25
|
+
Individual CRUD operation load tests:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bun run load-test-projects # Projects CRUD load test
|
|
29
|
+
bun run load-test-connections # Connections CRUD load test
|
|
30
|
+
bun run load-test-packages # Packages CRUD load test
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Combined load test (runs all three):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bun run load-test
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Performance Tests
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bun run spike-test # Spike test - sudden traffic spikes
|
|
43
|
+
bun run stress-test # Stress test - beyond normal capacity
|
|
44
|
+
bun run breakpoint-test # Breakpoint test - find maximum capacity
|
|
45
|
+
bun run soak-test # Soak test - sustained load over time
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Run All Tests
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bun run all-tests
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Environment Variables
|
|
55
|
+
|
|
56
|
+
- `PUBLISHER_URL` - Base URL of the publisher service (default: `http://localhost:4000`)
|
|
57
|
+
- `PROJECT_NAME` - Project name for tests (default: `malloy-samples`)
|
|
58
|
+
- `AUTH_TOKEN` - Authorization token for API requests
|
|
59
|
+
- `GOOGLE_APPLICATION_CREDENTIALS` - BigQuery service account JSON (for BigQuery tests)
|
|
60
|
+
- `USE_VERSION_ID` - Set to `"true"` to use version IDs in API calls
|
|
61
|
+
- `MAX_VIEWS_PER_MODEL` - Maximum views to test per model (default: `5`)
|
|
62
|
+
- `DEBUG` - Set to `"true"` for verbose logging
|
|
63
|
+
- `WHITELISTED_PACKAGES` - JSON array of package names to test
|
|
64
|
+
- `AVAILABLE_PACKAGES` - JSON array of available package names
|
|
65
|
+
|
|
66
|
+
## Client Generation
|
|
67
|
+
|
|
68
|
+
To regenerate API clients from OpenAPI spec:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bun run generate-clients
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Cleanup
|
|
75
|
+
|
|
76
|
+
Remove generated files and dependencies:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
bun run clean
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Build
|
|
83
|
+
|
|
84
|
+
Compile TypeScript:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bun run build
|
|
88
|
+
```
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { check, sleep } from "k6";
|
|
2
2
|
import {
|
|
3
|
+
getAvailablePackages,
|
|
3
4
|
getModelData,
|
|
4
5
|
getModels,
|
|
5
6
|
getPackages,
|
|
@@ -39,7 +40,11 @@ export const breakpointTest: TestPreset = {
|
|
|
39
40
|
},
|
|
40
41
|
run: () => {
|
|
41
42
|
const packages = getPackages();
|
|
43
|
+
const availablePackages = getAvailablePackages();
|
|
42
44
|
for (const pkg of packages) {
|
|
45
|
+
if (!availablePackages.includes(pkg.name)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
43
48
|
for (const model of getModels(pkg.name)) {
|
|
44
49
|
const modelData = getModelData(pkg.name, model.path);
|
|
45
50
|
for (const view of getViews(modelData)) {
|