@malloy-publisher/server 0.0.219 → 0.0.220

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 (59) hide show
  1. package/dist/app/api-doc.yaml +240 -19
  2. package/dist/app/assets/{EnvironmentPage-gehnjfC6.js → EnvironmentPage-BqLiaatL.js} +1 -1
  3. package/dist/app/assets/{HomePage-8LQBytE4.js → HomePage-G_xpt9XE.js} +1 -1
  4. package/dist/app/assets/LightMode-s1PDRIsF.js +1 -0
  5. package/dist/app/assets/MainPage-C6TfpC92.js +2 -0
  6. package/dist/app/assets/{MaterializationsPage-D7P1Kp6O.js → MaterializationsPage-BMO1afhm.js} +1 -1
  7. package/dist/app/assets/ModelPage-zHOJwMnU.js +1 -0
  8. package/dist/app/assets/{PackagePage-Cz9fVwSG.js → PackagePage-GCaWmELQ.js} +1 -1
  9. package/dist/app/assets/{RouteError-UONCloyN.js → RouteError-CIC588k4.js} +1 -1
  10. package/dist/app/assets/ThemeEditorPage-BdbIfIGO.js +1 -0
  11. package/dist/app/assets/{WorkbookPage-Bhzqvbq_.js → WorkbookPage-C8ucK_H8.js} +1 -1
  12. package/dist/app/assets/{core-BiGj7BML.es-kMHAa8tP.js → core-DFvqRVqM.es-NtfPVC7h.js} +1 -1
  13. package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
  14. package/dist/app/assets/index-8E2uLeV9.js +2541 -0
  15. package/dist/app/assets/index-B3NcDPbp.js +18 -0
  16. package/dist/app/assets/index-BxDMCn3s.js +527 -0
  17. package/dist/app/assets/index-aYNf0kTZ.js +1 -0
  18. package/dist/app/assets/index-aYtt-ovi.js +1761 -0
  19. package/dist/app/assets/index-w_0OQJgZ.js +23 -0
  20. package/dist/app/index.html +1 -1
  21. package/dist/runtime/publisher.js +6 -1
  22. package/dist/server.mjs +377 -50
  23. package/dist/sshcrypto-8m50vnmb.node +0 -0
  24. package/package.json +12 -12
  25. package/src/config.theme.spec.ts +178 -0
  26. package/src/config.ts +179 -0
  27. package/src/controller/materialization.controller.spec.ts +4 -4
  28. package/src/controller/materialization.controller.ts +2 -2
  29. package/src/controller/theme.controller.ts +83 -0
  30. package/src/dto/connection.dto.ts +6 -0
  31. package/src/materialization_metrics.ts +1 -1
  32. package/src/runtime/publisher.js +6 -1
  33. package/src/server.ts +58 -1
  34. package/src/service/build_plan.spec.ts +17 -17
  35. package/src/service/build_plan.ts +25 -10
  36. package/src/service/connection.ts +52 -6
  37. package/src/service/connection_fingerprint.spec.ts +102 -0
  38. package/src/service/manifest_loader.spec.ts +5 -5
  39. package/src/service/manifest_loader.ts +4 -4
  40. package/src/service/materialization_service.spec.ts +47 -37
  41. package/src/service/materialization_service.ts +59 -50
  42. package/src/service/materialization_test_fixtures.ts +5 -5
  43. package/src/service/model.ts +1 -1
  44. package/src/service/package.ts +4 -4
  45. package/src/service/theme_store.ts +199 -0
  46. package/src/storage/DatabaseInterface.ts +1 -1
  47. package/src/storage/StorageManager.ts +17 -0
  48. package/src/storage/duckdb/schema.ts +27 -6
  49. package/src/theme_key_parity.spec.ts +57 -0
  50. package/tests/integration/materialization/manifest_binding.integration.spec.ts +1 -1
  51. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +5 -5
  52. package/tests/unit/controller/theme.controller.spec.ts +141 -0
  53. package/tests/unit/service/theme_store.spec.ts +139 -0
  54. package/dist/app/assets/MainPage-DDaZLJw-.js +0 -2
  55. package/dist/app/assets/ModelPage-Do_vhxOc.js +0 -1
  56. package/dist/app/assets/index-VzRbxcF7.js +0 -455
  57. package/dist/app/assets/index-ddq4-5hu.js +0 -1760
  58. package/dist/app/assets/index-qOQF9CXq.js +0 -40
  59. package/dist/app/assets/index.umd-B2kmxDh7.js +0 -2467
package/dist/server.mjs CHANGED
@@ -158407,6 +158407,11 @@ var require_utils74 = __commonJS((exports, module) => {
158407
158407
  };
158408
158408
  });
158409
158409
 
158410
+ // ../../node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
158411
+ var require_sshcrypto = __commonJS((exports, module) => {
158412
+ module.exports = __require("./sshcrypto-8m50vnmb.node");
158413
+ });
158414
+
158410
158415
  // ../../node_modules/ssh2/lib/protocol/crypto/poly1305.js
158411
158416
  var require_poly1305 = __commonJS((exports, module) => {
158412
158417
  var __dirname = "/home/runner/work/publisher/publisher/node_modules/ssh2/lib/protocol/crypto", __filename = "/home/runner/work/publisher/publisher/node_modules/ssh2/lib/protocol/crypto/poly1305.js";
@@ -158893,7 +158898,7 @@ var require_crypto = __commonJS((exports, module) => {
158893
158898
  var ChaChaPolyDecipher;
158894
158899
  var GenericDecipher;
158895
158900
  try {
158896
- binding = (()=>{throw new Error("Cannot require module "+"./crypto/build/Release/sshcrypto.node");})();
158901
+ binding = require_sshcrypto();
158897
158902
  ({
158898
158903
  AESGCMCipher,
158899
158904
  ChaChaPolyCipher,
@@ -242924,6 +242929,15 @@ function resolvePublisherConfigPath(serverRoot) {
242924
242929
  }
242925
242930
  return null;
242926
242931
  }
242932
+ var PER_MODE_COLOR_KEYS = [
242933
+ "background",
242934
+ "tableHeader",
242935
+ "tableHeaderBackground",
242936
+ "tableBody",
242937
+ "tile",
242938
+ "tileTitle",
242939
+ "mapColor"
242940
+ ];
242927
242941
  var DEFAULT_HIGH_WATER_FRACTION = 0.8;
242928
242942
  var DEFAULT_LOW_WATER_FRACTION = 0.7;
242929
242943
  var DEFAULT_CHECK_INTERVAL_MS = 5000;
@@ -243113,11 +243127,98 @@ var getPublisherConfig = (serverRoot) => {
243113
243127
  if (processedConfig && typeof processedConfig === "object" && "frozenConfig" in processedConfig) {
243114
243128
  frozenConfig = Boolean(processedConfig.frozenConfig);
243115
243129
  }
243130
+ const instanceTheme = sanitizeTheme(processedConfig && typeof processedConfig === "object" && "theme" in processedConfig ? processedConfig.theme : undefined, "publisher.config.json");
243116
243131
  return {
243117
243132
  frozenConfig,
243133
+ ...instanceTheme ? { theme: instanceTheme } : {},
243118
243134
  environments
243119
243135
  };
243120
243136
  };
243137
+ function sanitizeTheme(raw, context) {
243138
+ if (raw === undefined || raw === null)
243139
+ return;
243140
+ if (typeof raw !== "object" || Array.isArray(raw)) {
243141
+ logger.warn(`Invalid "theme" in ${context}: expected an object. Ignoring.`);
243142
+ return;
243143
+ }
243144
+ const obj = raw;
243145
+ const theme = {};
243146
+ if ("defaultMode" in obj) {
243147
+ const mode = obj.defaultMode;
243148
+ if (mode === "light" || mode === "dark" || mode === "auto") {
243149
+ theme.defaultMode = mode;
243150
+ } else {
243151
+ logger.warn(`Invalid "theme.defaultMode" in ${context}: expected "light" | "dark" | "auto" (got ${JSON.stringify(mode)}). Ignoring field.`);
243152
+ }
243153
+ }
243154
+ if ("allowUserToggle" in obj) {
243155
+ const value = obj.allowUserToggle;
243156
+ if (typeof value === "boolean") {
243157
+ theme.allowUserToggle = value;
243158
+ } else {
243159
+ logger.warn(`Invalid "theme.allowUserToggle" in ${context}: expected a boolean (got ${JSON.stringify(value)}). Ignoring field.`);
243160
+ }
243161
+ }
243162
+ if ("palette" in obj && obj.palette && typeof obj.palette === "object") {
243163
+ const palette = obj.palette;
243164
+ const sanitized = {};
243165
+ if (Array.isArray(palette.series)) {
243166
+ sanitized.series = palette.series.filter((c) => typeof c === "string");
243167
+ }
243168
+ for (const key of PER_MODE_COLOR_KEYS) {
243169
+ const raw2 = palette[key];
243170
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2))
243171
+ continue;
243172
+ const r = raw2;
243173
+ const out = {};
243174
+ if (typeof r.light === "string")
243175
+ out.light = r.light;
243176
+ if (typeof r.dark === "string")
243177
+ out.dark = r.dark;
243178
+ if (Object.keys(out).length > 0) {
243179
+ sanitized[key] = out;
243180
+ }
243181
+ }
243182
+ if (Object.keys(sanitized).length > 0)
243183
+ theme.palette = sanitized;
243184
+ }
243185
+ if ("font" in obj && obj.font && typeof obj.font === "object") {
243186
+ const font = obj.font;
243187
+ const sanitized = {};
243188
+ if (typeof font.family === "string")
243189
+ sanitized.family = font.family;
243190
+ if (typeof font.size === "number" && Number.isFinite(font.size)) {
243191
+ sanitized.size = font.size;
243192
+ }
243193
+ if (Object.keys(sanitized).length > 0)
243194
+ theme.font = sanitized;
243195
+ }
243196
+ return Object.keys(theme).length > 0 ? theme : undefined;
243197
+ }
243198
+ function mergeThemes(base, override) {
243199
+ if (!base)
243200
+ return override;
243201
+ if (!override)
243202
+ return base;
243203
+ const merged = { ...base, ...override };
243204
+ if (base.palette || override.palette) {
243205
+ merged.palette = {
243206
+ ...base.palette ?? {},
243207
+ ...override.palette ?? {}
243208
+ };
243209
+ for (const key of PER_MODE_COLOR_KEYS) {
243210
+ const b = base.palette?.[key];
243211
+ const o = override.palette?.[key];
243212
+ if (b || o) {
243213
+ merged.palette[key] = { ...b ?? {}, ...o ?? {} };
243214
+ }
243215
+ }
243216
+ }
243217
+ if (base.font || override.font) {
243218
+ merged.font = { ...base.font ?? {}, ...override.font ?? {} };
243219
+ }
243220
+ return merged;
243221
+ }
243121
243222
  var isPublisherConfigFrozen = (serverRoot) => {
243122
243223
  try {
243123
243224
  const publisherConfig = getPublisherConfig(serverRoot);
@@ -243193,17 +243294,29 @@ var getProcessedPublisherConfig = (serverRoot) => {
243193
243294
  logger.warn(`Environment "${environment.name}" has no valid packages. Skipping entry.`);
243194
243295
  continue;
243195
243296
  }
243297
+ const envTheme = sanitizeTheme(environment.theme, `environment "${environment.name}"`);
243298
+ const resolvedTheme = mergeThemes(rawConfig.theme, envTheme);
243196
243299
  validEnvironments.push({
243197
243300
  name: environment.name,
243198
243301
  packages: validPackages,
243199
- connections: convertConnectionsToApiConnections(environment.connections || [])
243302
+ connections: convertConnectionsToApiConnections(environment.connections || []),
243303
+ ...resolvedTheme ? { theme: resolvedTheme } : {}
243200
243304
  });
243201
243305
  }
243202
243306
  return {
243203
243307
  frozenConfig: rawConfig.frozenConfig ?? false,
243308
+ ...rawConfig.theme ? { theme: rawConfig.theme } : {},
243204
243309
  environments: validEnvironments
243205
243310
  };
243206
243311
  };
243312
+ var getInstanceTheme = (serverRoot) => {
243313
+ try {
243314
+ return getPublisherConfig(serverRoot).theme;
243315
+ } catch (error) {
243316
+ logger.error(`Error reading instance theme from ${PUBLISHER_CONFIG_NAME}`, { error });
243317
+ return;
243318
+ }
243319
+ };
243207
243320
 
243208
243321
  // src/controller/connection.controller.ts
243209
243322
  init_errors();
@@ -247871,6 +247984,13 @@ function buildAzureDuckDBConnection(metadata, entry) {
247871
247984
  function getMetadataForLookup(metadata, name) {
247872
247985
  return name ? metadata.get(name) : undefined;
247873
247986
  }
247987
+ function applyConnectionFingerprint(connection, metadata) {
247988
+ const fingerprint = metadata?.apiConnection.fingerprint;
247989
+ if (fingerprint) {
247990
+ connection.getDigest = () => fingerprint;
247991
+ }
247992
+ return connection;
247993
+ }
247874
247994
  function isDuckDBConnection(connection) {
247875
247995
  return connection instanceof DuckDBConnection;
247876
247996
  }
@@ -247896,9 +248016,8 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
247896
248016
  }
247897
248017
  await attachPromise;
247898
248018
  }
247899
- malloyConfig.wrapConnections((base) => ({
247900
- lookupConnection: async (name) => {
247901
- const metadata = getMetadataForLookup(assembled.metadata, name);
248019
+ malloyConfig.wrapConnections((base) => {
248020
+ const resolveConnection = async (name, metadata) => {
247902
248021
  if (metadata?.isDuckLake) {
247903
248022
  let connectionPromise = duckLakeCache.get(name);
247904
248023
  if (!connectionPromise) {
@@ -247970,8 +248089,14 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
247970
248089
  await attachOnce(connection, metadata);
247971
248090
  }
247972
248091
  return connection;
247973
- }
247974
- }));
248092
+ };
248093
+ return {
248094
+ lookupConnection: async (name) => {
248095
+ const metadata = getMetadataForLookup(assembled.metadata, name);
248096
+ return applyConnectionFingerprint(await resolveConnection(name, metadata), metadata);
248097
+ }
248098
+ };
248099
+ });
247975
248100
  return {
247976
248101
  malloyConfig,
247977
248102
  apiConnections: assembled.apiConnections,
@@ -256512,13 +256637,10 @@ class DuckDBRepository {
256512
256637
  init_logger();
256513
256638
  async function initializeSchema(db, force = false) {
256514
256639
  const initialized = await db.isInitialized();
256515
- if (initialized && !force) {
256516
- return;
256517
- }
256518
256640
  if (force) {
256519
256641
  logger.info("Reinitializing database schema dropping and recreating all tables");
256520
256642
  await dropAllTables(db);
256521
- } else {
256643
+ } else if (!initialized) {
256522
256644
  await dropLegacyProjectSchema(db);
256523
256645
  logger.info("Creating database schema for the first time...");
256524
256646
  }
@@ -256577,6 +256699,14 @@ async function initializeSchema(db, force = false) {
256577
256699
  FOREIGN KEY (environment_id) REFERENCES environments(id)
256578
256700
  )
256579
256701
  `);
256702
+ await db.run(`
256703
+ CREATE TABLE IF NOT EXISTS themes (
256704
+ id VARCHAR PRIMARY KEY,
256705
+ payload JSON NOT NULL,
256706
+ created_at TIMESTAMP NOT NULL,
256707
+ updated_at TIMESTAMP NOT NULL
256708
+ )
256709
+ `);
256580
256710
  await db.run("CREATE INDEX IF NOT EXISTS idx_packages_environment_id ON packages(environment_id)");
256581
256711
  await db.run("CREATE INDEX IF NOT EXISTS idx_connections_environment_id ON connections(environment_id)");
256582
256712
  await db.run("CREATE INDEX IF NOT EXISTS idx_materializations_environment_package ON materializations(environment_id, package_name)");
@@ -256609,7 +256739,8 @@ async function dropAllTables(db) {
256609
256739
  "materializations",
256610
256740
  "packages",
256611
256741
  "connections",
256612
- "environments"
256742
+ "environments",
256743
+ "themes"
256613
256744
  ];
256614
256745
  logger.info("Dropping tables:", tables.join(", "));
256615
256746
  for (const table of tables) {
@@ -256655,6 +256786,15 @@ class StorageManager {
256655
256786
  this.connection = connection;
256656
256787
  this.repository = new DuckDBRepository(connection);
256657
256788
  }
256789
+ getDuckDbConnection() {
256790
+ if (!this.connection) {
256791
+ throw new Error("Storage not initialized");
256792
+ }
256793
+ if (!(this.connection instanceof DuckDBConnection2)) {
256794
+ throw new Error("Underlying storage connection is not DuckDB");
256795
+ }
256796
+ return this.connection;
256797
+ }
256658
256798
  getRepository() {
256659
256799
  if (!this.repository) {
256660
256800
  throw new Error("Storage not initialized. Call initialize() first.");
@@ -256801,16 +256941,16 @@ async function fetchManifestEntries(uri) {
256801
256941
  throw new Error(`Failed to parse build manifest at ${uri}: ${err instanceof Error ? err.message : String(err)}`);
256802
256942
  }
256803
256943
  const entries = {};
256804
- for (const [buildId, entry] of Object.entries(parsed.entries ?? {})) {
256944
+ for (const [sourceEntityId, entry] of Object.entries(parsed.entries ?? {})) {
256805
256945
  const physicalTableName = entry?.physicalTableName;
256806
256946
  if (!physicalTableName) {
256807
256947
  logger.warn("Manifest entry has no physicalTableName; skipping", {
256808
256948
  uri,
256809
- buildId
256949
+ sourceEntityId
256810
256950
  });
256811
256951
  continue;
256812
256952
  }
256813
- entries[buildId] = { tableName: physicalTableName };
256953
+ entries[sourceEntityId] = { tableName: physicalTableName };
256814
256954
  }
256815
256955
  return entries;
256816
256956
  }
@@ -258302,7 +258442,7 @@ function* iterGraphSources(graph, sources) {
258302
258442
  }
258303
258443
  }
258304
258444
  }
258305
- function computeBuildId(source, connectionDigests) {
258445
+ function computeSourceEntityId(source, connectionDigests) {
258306
258446
  return source.makeBuildId(connectionDigests[source.connectionName], source.getSQL());
258307
258447
  }
258308
258448
  async function resolvePackageConnections(pkg, names) {
@@ -258390,7 +258530,7 @@ function deriveBuildPlan(graphs, sources, connectionDigests, sourceNames, source
258390
258530
  sourceID: source.sourceID,
258391
258531
  connectionName: source.connectionName,
258392
258532
  dialect: source.dialectName,
258393
- buildId: computeBuildId(source, connectionDigests),
258533
+ sourceEntityId: computeSourceEntityId(source, connectionDigests),
258394
258534
  sql: source.getSQL(),
258395
258535
  columns: deriveColumns(source),
258396
258536
  annotationFields: deriveAnnotationFields(source),
@@ -261030,7 +261170,7 @@ class MaterializationController {
261030
261170
  }
261031
261171
  const instruction = raw;
261032
261172
  const required = [
261033
- "buildId",
261173
+ "sourceEntityId",
261034
261174
  "materializedTableId",
261035
261175
  "physicalTableName",
261036
261176
  "realization"
@@ -261044,7 +261184,7 @@ class MaterializationController {
261044
261184
  throw new BadRequestError("Build instruction 'realization' must be COPY or SNAPSHOT");
261045
261185
  }
261046
261186
  return {
261047
- buildId: instruction.buildId,
261187
+ sourceEntityId: instruction.sourceEntityId,
261048
261188
  sourceID: typeof instruction.sourceID === "string" ? instruction.sourceID : undefined,
261049
261189
  materializedTableId: instruction.materializedTableId,
261050
261190
  physicalTableName: instruction.physicalTableName,
@@ -261065,6 +261205,40 @@ class MaterializationController {
261065
261205
  }
261066
261206
  }
261067
261207
 
261208
+ // src/controller/theme.controller.ts
261209
+ init_errors();
261210
+ class ThemeController {
261211
+ themeStore;
261212
+ serverRoot;
261213
+ constructor(themeStore, serverRoot) {
261214
+ this.themeStore = themeStore;
261215
+ this.serverRoot = serverRoot;
261216
+ }
261217
+ getTheme = async () => {
261218
+ const theme = await this.themeStore.get();
261219
+ return theme ?? {};
261220
+ };
261221
+ putTheme = async (body) => {
261222
+ if (isPublisherConfigFrozen(this.serverRoot)) {
261223
+ throw new FrozenConfigError('Cannot edit theme: publisher.config.json has "frozenConfig": true.');
261224
+ }
261225
+ const isEmptyObject2 = typeof body === "object" && body !== null && !Array.isArray(body) && Object.keys(body).length === 0;
261226
+ const sanitized = sanitizeTheme(body, "PUT /api/v0/theme");
261227
+ if (!sanitized && !isEmptyObject2) {
261228
+ throw new BadRequestError("Theme payload was malformed (expected an object).");
261229
+ }
261230
+ const saved = await this.themeStore.set(sanitized ?? {});
261231
+ return saved;
261232
+ };
261233
+ resetTheme = async () => {
261234
+ if (isPublisherConfigFrozen(this.serverRoot)) {
261235
+ throw new FrozenConfigError('Cannot reset theme: publisher.config.json has "frozenConfig": true.');
261236
+ }
261237
+ const reseeded = await this.themeStore.reset();
261238
+ return reseeded ?? {};
261239
+ };
261240
+ }
261241
+
261068
261242
  // ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
261069
261243
  var DEFAULT_REQUEST_TIMEOUT_MSEC = 60000;
261070
261244
 
@@ -267628,9 +267802,9 @@ async function resolveEnvironmentId(repository, environmentName) {
267628
267802
  }
267629
267803
 
267630
267804
  // src/service/materialization_service.ts
267631
- var STAGING_BUILD_ID_LEN = 12;
267632
- function stagingSuffix(buildId) {
267633
- return `_${buildId.substring(0, STAGING_BUILD_ID_LEN)}`;
267805
+ var STAGING_ID_LEN = 12;
267806
+ function stagingSuffix(sourceEntityId) {
267807
+ return `_${sourceEntityId.replace(/-/g, "").substring(0, STAGING_ID_LEN)}`;
267634
267808
  }
267635
267809
  function selfAssignTableName(persistSource) {
267636
267810
  return deriveAnnotationFields(persistSource).name || persistSource.name;
@@ -267792,18 +267966,18 @@ class MaterializationService {
267792
267966
  for (const persistSource of iterGraphSources(graph, compiled.sources)) {
267793
267967
  if (include && !include.has(persistSource.name))
267794
267968
  continue;
267795
- const buildId = computeBuildId(persistSource, compiled.connectionDigests);
267796
- if (seen.has(buildId))
267969
+ const sourceEntityId = computeSourceEntityId(persistSource, compiled.connectionDigests);
267970
+ if (seen.has(sourceEntityId))
267797
267971
  continue;
267798
- seen.add(buildId);
267799
- const prior = priorEntries[buildId];
267972
+ seen.add(sourceEntityId);
267973
+ const prior = priorEntries[sourceEntityId];
267800
267974
  if (prior && prior.physicalTableName) {
267801
- carried[buildId] = prior;
267975
+ carried[sourceEntityId] = prior;
267802
267976
  continue;
267803
267977
  }
267804
267978
  instructions.push({
267805
- buildId,
267806
- materializedTableId: `local-${buildId.substring(0, STAGING_BUILD_ID_LEN)}`,
267979
+ sourceEntityId,
267980
+ materializedTableId: `local-${sourceEntityId.substring(0, STAGING_ID_LEN)}`,
267807
267981
  physicalTableName: selfAssignTableName(persistSource),
267808
267982
  realization: "COPY"
267809
267983
  });
@@ -267824,9 +267998,11 @@ class MaterializationService {
267824
267998
  }
267825
267999
  async autoLoadManifest(environment, packageName, entries) {
267826
268000
  const manifestEntries = {};
267827
- for (const [buildId, entry] of Object.entries(entries)) {
268001
+ for (const [sourceEntityId, entry] of Object.entries(entries)) {
267828
268002
  if (entry.physicalTableName) {
267829
- manifestEntries[buildId] = { tableName: entry.physicalTableName };
268003
+ manifestEntries[sourceEntityId] = {
268004
+ tableName: entry.physicalTableName
268005
+ };
267830
268006
  }
267831
268007
  }
267832
268008
  try {
@@ -267848,13 +268024,13 @@ class MaterializationService {
267848
268024
  if (!plan) {
267849
268025
  throw new BadRequestError("Package has no persist sources; buildInstructions cannot be applied");
267850
268026
  }
267851
- const plannedBuildIds = new Set;
268027
+ const plannedSourceEntityIds = new Set;
267852
268028
  for (const source of Object.values(plan.sources)) {
267853
- plannedBuildIds.add(source.buildId);
268029
+ plannedSourceEntityIds.add(source.sourceEntityId);
267854
268030
  }
267855
268031
  for (const instruction of instructions) {
267856
- if (!plannedBuildIds.has(instruction.buildId)) {
267857
- throw new BadRequestError(`Instruction references unknown buildId '${instruction.buildId}'`);
268032
+ if (!plannedSourceEntityIds.has(instruction.sourceEntityId)) {
268033
+ throw new BadRequestError(`Instruction references unknown sourceEntityId '${instruction.sourceEntityId}'`);
267858
268034
  }
267859
268035
  if (instruction.realization === "SNAPSHOT") {
267860
268036
  throw new BadRequestError("realization=SNAPSHOT is not supported (COPY only)");
@@ -267864,20 +268040,22 @@ class MaterializationService {
267864
268040
  async executeInstructedBuild(compiled, instructions, seedEntries, signal) {
267865
268041
  const { graphs, sources, connectionDigests, connections } = compiled;
267866
268042
  const bySourceID = new Map;
267867
- const byBuildId = new Map;
268043
+ const bySourceEntityId = new Map;
267868
268044
  for (const instruction of instructions) {
267869
268045
  if (instruction.sourceID) {
267870
268046
  bySourceID.set(instruction.sourceID, instruction);
267871
268047
  }
267872
- byBuildId.set(instruction.buildId, instruction);
268048
+ bySourceEntityId.set(instruction.sourceEntityId, instruction);
267873
268049
  }
267874
268050
  const manifest = new Manifest;
267875
268051
  const entries = {};
267876
- for (const [buildId, entry] of Object.entries(seedEntries)) {
268052
+ for (const [sourceEntityId, entry] of Object.entries(seedEntries)) {
267877
268053
  if (entry.physicalTableName) {
267878
- manifest.update(buildId, { tableName: entry.physicalTableName });
268054
+ manifest.update(sourceEntityId, {
268055
+ tableName: entry.physicalTableName
268056
+ });
267879
268057
  }
267880
- entries[buildId] = entry;
268058
+ entries[sourceEntityId] = entry;
267881
268059
  }
267882
268060
  for (const graph of graphs) {
267883
268061
  const connection = connections.get(graph.connectionName);
@@ -267887,25 +268065,25 @@ class MaterializationService {
267887
268065
  for (const persistSource of iterGraphSources(graph, sources)) {
267888
268066
  if (signal.aborted)
267889
268067
  throw new Error("Build cancelled");
267890
- const buildId = computeBuildId(persistSource, connectionDigests);
267891
- const instruction = bySourceID.get(persistSource.sourceID) ?? byBuildId.get(buildId);
268068
+ const sourceEntityId = computeSourceEntityId(persistSource, connectionDigests);
268069
+ const instruction = bySourceID.get(persistSource.sourceID) ?? bySourceEntityId.get(sourceEntityId);
267892
268070
  if (!instruction)
267893
268071
  continue;
267894
268072
  const entry = await this.buildOneSource(persistSource, instruction, connection, connectionDigests, manifest);
267895
- entries[buildId] = entry;
268073
+ entries[sourceEntityId] = entry;
267896
268074
  }
267897
268075
  }
267898
268076
  return entries;
267899
268077
  }
267900
268078
  async buildOneSource(persistSource, instruction, connection, connectionDigests, manifest) {
267901
- const buildId = instruction.buildId;
268079
+ const sourceEntityId = instruction.sourceEntityId;
267902
268080
  const physicalTableName = instruction.physicalTableName;
267903
268081
  const buildSQL = persistSource.getSQL({
267904
268082
  buildManifest: manifest.buildManifest,
267905
268083
  connectionDigests
267906
268084
  });
267907
268085
  const bareName = bareTableName(physicalTableName);
267908
- const stagingTableName = `${physicalTableName}${stagingSuffix(buildId)}`;
268086
+ const stagingTableName = `${physicalTableName}${stagingSuffix(sourceEntityId)}`;
267909
268087
  const dialect = persistSource.dialectName;
267910
268088
  const quotedStaging = quoteTablePath(stagingTableName, dialect);
267911
268089
  const quotedPhysical = quoteTablePath(physicalTableName, dialect);
@@ -267928,7 +268106,7 @@ class MaterializationService {
267928
268106
  }
267929
268107
  throw err;
267930
268108
  }
267931
- manifest.update(buildId, { tableName: physicalTableName });
268109
+ manifest.update(sourceEntityId, { tableName: physicalTableName });
267932
268110
  const durationMs = Math.round(performance.now() - startTime);
267933
268111
  recordSourceBuildDuration(durationMs);
267934
268112
  logger.info(`Built materialized source ${persistSource.name}`, {
@@ -267936,7 +268114,7 @@ class MaterializationService {
267936
268114
  durationMs
267937
268115
  });
267938
268116
  return {
267939
- buildId,
268117
+ sourceEntityId,
267940
268118
  sourceName: persistSource.name,
267941
268119
  materializedTableId: instruction.materializedTableId,
267942
268120
  physicalTableName,
@@ -267993,7 +268171,7 @@ class MaterializationService {
267993
268171
  if (!connectionName || !physicalTableName) {
267994
268172
  logger.warn("Skipping manifest entry with no connection/table", {
267995
268173
  materializationId: m.id,
267996
- buildId: entry.buildId
268174
+ sourceEntityId: entry.sourceEntityId
267997
268175
  });
267998
268176
  continue;
267999
268177
  }
@@ -268005,7 +268183,7 @@ class MaterializationService {
268005
268183
  }
268006
268184
  const dialect = connection.dialectName;
268007
268185
  await connection.runSQL(`DROP TABLE IF EXISTS ${quoteTablePath(physicalTableName, dialect)}`);
268008
- await connection.runSQL(`DROP TABLE IF EXISTS ${quoteTablePath(`${physicalTableName}${stagingSuffix(entry.buildId)}`, dialect)}`);
268186
+ await connection.runSQL(`DROP TABLE IF EXISTS ${quoteTablePath(`${physicalTableName}${stagingSuffix(entry.sourceEntityId)}`, dialect)}`);
268009
268187
  recordDropTables("success");
268010
268188
  logger.info("Dropped materialized table on delete", {
268011
268189
  materializationId: m.id,
@@ -268172,6 +268350,122 @@ class PackageMemoryGovernor {
268172
268350
  }
268173
268351
  }
268174
268352
 
268353
+ // src/service/theme_store.ts
268354
+ init_logger();
268355
+ var THEME_ROW_ID = "default";
268356
+
268357
+ class ThemeStore {
268358
+ storageManager;
268359
+ serverRoot;
268360
+ mutex = new Mutex;
268361
+ cached;
268362
+ cacheLoaded = false;
268363
+ constructor(storageManager, serverRoot) {
268364
+ this.storageManager = storageManager;
268365
+ this.serverRoot = serverRoot;
268366
+ }
268367
+ get db() {
268368
+ return this.storageManager.getDuckDbConnection();
268369
+ }
268370
+ async initialize() {
268371
+ await this.mutex.runExclusive(async () => {
268372
+ await this.loadLocked();
268373
+ });
268374
+ }
268375
+ async get() {
268376
+ if (!this.cacheLoaded) {
268377
+ await this.initialize();
268378
+ }
268379
+ return this.cached;
268380
+ }
268381
+ async set(theme) {
268382
+ return this.mutex.runExclusive(async () => {
268383
+ await this.persistLocked(theme);
268384
+ this.cached = theme;
268385
+ this.cacheLoaded = true;
268386
+ return theme;
268387
+ });
268388
+ }
268389
+ async reset() {
268390
+ return this.mutex.runExclusive(async () => {
268391
+ await this.db.run("DELETE FROM themes WHERE id = ?", [THEME_ROW_ID]);
268392
+ this.cached = undefined;
268393
+ this.cacheLoaded = false;
268394
+ await this.loadLocked();
268395
+ return this.cached;
268396
+ });
268397
+ }
268398
+ async loadLocked() {
268399
+ if (this.cacheLoaded)
268400
+ return;
268401
+ const row = await this.db.get("SELECT payload FROM themes WHERE id = ?", [THEME_ROW_ID]);
268402
+ if (row?.payload) {
268403
+ let parsed;
268404
+ try {
268405
+ parsed = JSON.parse(row.payload);
268406
+ } catch (error) {
268407
+ logger.warn("ThemeStore: stored theme JSON is unparseable; treating as empty", { error });
268408
+ this.cacheLoaded = true;
268409
+ return;
268410
+ }
268411
+ const sanitized = sanitizeTheme(parsed, "ThemeStore.load");
268412
+ const dropped = listSanitizerDrops(parsed, sanitized);
268413
+ if (dropped.length > 0) {
268414
+ logger.info("ThemeStore: dropped stale fields from stored theme during load", { dropped });
268415
+ }
268416
+ this.cached = sanitized;
268417
+ } else {
268418
+ const seed = getInstanceTheme(this.serverRoot);
268419
+ if (seed) {
268420
+ this.cached = seed;
268421
+ await this.persistLocked(seed);
268422
+ }
268423
+ }
268424
+ this.cacheLoaded = true;
268425
+ }
268426
+ async persistLocked(theme) {
268427
+ const now = new Date().toISOString();
268428
+ const payload = JSON.stringify(theme);
268429
+ await this.db.run(`INSERT INTO themes (id, payload, created_at, updated_at)
268430
+ VALUES (?, ?, ?, ?)
268431
+ ON CONFLICT (id) DO UPDATE SET
268432
+ payload = EXCLUDED.payload,
268433
+ updated_at = EXCLUDED.updated_at`, [THEME_ROW_ID, payload, now, now]);
268434
+ }
268435
+ }
268436
+ function listSanitizerDrops(raw, sanitized) {
268437
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
268438
+ return [];
268439
+ const r = raw;
268440
+ const drops = [];
268441
+ const topKeys = ["defaultMode", "allowUserToggle"];
268442
+ for (const k of topKeys) {
268443
+ if (r[k] !== undefined && sanitized?.[k] === undefined)
268444
+ drops.push(k);
268445
+ }
268446
+ if (r.palette && typeof r.palette === "object" && !Array.isArray(r.palette)) {
268447
+ const rp = r.palette;
268448
+ const sp = sanitized?.palette ?? {};
268449
+ for (const k of Object.keys(rp)) {
268450
+ if (rp[k] !== undefined && sp[k] === undefined)
268451
+ drops.push(`palette.${k}`);
268452
+ }
268453
+ } else if (r.palette !== undefined && (!sanitized || sanitized.palette === undefined)) {
268454
+ drops.push("palette");
268455
+ }
268456
+ if (r.font && typeof r.font === "object" && !Array.isArray(r.font)) {
268457
+ const rf = r.font;
268458
+ const sf = sanitized?.font ?? {};
268459
+ for (const k of Object.keys(rf)) {
268460
+ if (rf[k] !== undefined && sf[k] === undefined)
268461
+ drops.push(`font.${k}`);
268462
+ }
268463
+ } else if (r.font !== undefined && (!sanitized || sanitized.font === undefined)) {
268464
+ drops.push("font");
268465
+ }
268466
+ return drops;
268467
+ }
268468
+
268175
268469
  // src/server.ts
268176
268470
  function parseArgs() {
268177
268471
  const args = process.argv.slice(2);
@@ -268266,6 +268560,8 @@ var queryController = new QueryController(environmentStore);
268266
268560
  var compileController = new CompileController(environmentStore);
268267
268561
  var materializationService = new MaterializationService(environmentStore);
268268
268562
  var materializationController = new MaterializationController(materializationService);
268563
+ var themeStore = new ThemeStore(environmentStore.storageManager, SERVER_ROOT);
268564
+ var themeController = new ThemeController(themeStore, SERVER_ROOT);
268269
268565
  var mcpApp = import_express2.default();
268270
268566
  registerHealthEndpoints(mcpApp);
268271
268567
  mcpApp.use(MCP_ENDPOINT, import_express2.default.json());
@@ -268539,13 +268835,44 @@ app.get(`${API_PREFIX2}/environments/:environmentName/packages/:packageName/page
268539
268835
  app.get(`${API_PREFIX2}/status`, async (_req, res) => {
268540
268836
  try {
268541
268837
  const status = await environmentStore.getStatus();
268542
- res.status(200).json(status);
268838
+ const theme = environmentStore.storageManager.isInitialized() ? await themeStore.get() : undefined;
268839
+ res.status(200).json({ ...status, theme: theme ?? {} });
268543
268840
  } catch (error) {
268544
268841
  logger.error("Error getting status", { error });
268545
268842
  const { json, status } = internalErrorToHttpError(error);
268546
268843
  res.status(status).json(json);
268547
268844
  }
268548
268845
  });
268846
+ app.get(`${API_PREFIX2}/theme`, async (_req, res) => {
268847
+ try {
268848
+ if (!environmentStore.storageManager.isInitialized()) {
268849
+ throw new ServiceUnavailableError("Theme storage is still initializing. Retry shortly.");
268850
+ }
268851
+ res.status(200).json(await themeController.getTheme());
268852
+ } catch (error) {
268853
+ logger.error("Error getting theme", { error });
268854
+ const { json, status } = internalErrorToHttpError(error);
268855
+ res.status(status).json(json);
268856
+ }
268857
+ });
268858
+ app.put(`${API_PREFIX2}/theme`, async (req, res) => {
268859
+ try {
268860
+ res.status(200).json(await themeController.putTheme(req.body));
268861
+ } catch (error) {
268862
+ logger.error("Error saving theme", { error });
268863
+ const { json, status } = internalErrorToHttpError(error);
268864
+ res.status(status).json(json);
268865
+ }
268866
+ });
268867
+ app.delete(`${API_PREFIX2}/theme`, async (_req, res) => {
268868
+ try {
268869
+ res.status(200).json(await themeController.resetTheme());
268870
+ } catch (error) {
268871
+ logger.error("Error resetting theme", { error });
268872
+ const { json, status } = internalErrorToHttpError(error);
268873
+ res.status(status).json(json);
268874
+ }
268875
+ });
268549
268876
  app.get(`${API_PREFIX2}/watch-mode/status`, watchModeController.getWatchStatus);
268550
268877
  app.post(`${API_PREFIX2}/watch-mode/start`, watchModeController.startWatching);
268551
268878
  app.post(`${API_PREFIX2}/watch-mode/stop`, watchModeController.stopWatchMode);