@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.
@@ -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: endTime - startTime,
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: endTime - startTime,
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 new Promise((resolve3) => {
136103
- this.connection.all("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'", (err, rows) => {
136104
- if (err) {
136105
- resolve3(false);
136106
- return;
136107
- }
136108
- resolve3(rows && rows.length > 0);
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 new Promise((resolve3, reject) => {
136117
- const callback = (err) => {
136118
- if (err) {
136119
- reject(new Error(`Query execution failed: ${err.message}
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
- return;
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
- resolve3();
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 new Promise((resolve3, reject) => {
136137
- const callback = (err, rows) => {
136138
- if (err) {
136139
- reject(new Error(`Query execution failed: ${err.message}
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
- return;
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
- resolve3(rows || []);
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.modelDef ? import_malloy2.modelDefToModelInfo(this.modelDef).entries.filter((entry) => {
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 modelDuckDBConnection = new import_db_duckdb2.DuckDBConnection(name, ":memory:", workingDirectory);
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
- const deleted = this.malloyConnections.delete(connectionName);
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 (deleted || index !== -1) {
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
- logger.info(`Project store successfully initialized in ${performance.now() - initialTime}ms`);
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)) {