@malloy-publisher/server 0.0.196-dev → 0.0.196

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 (103) hide show
  1. package/dist/app/api-doc.yaml +213 -214
  2. package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +1 -0
  3. package/dist/app/assets/HomePage-DMop21VG.js +1 -0
  4. package/dist/app/assets/MainPage-BbE8ETz1.js +2 -0
  5. package/dist/app/assets/ModelPage-D2jvfe3t.js +1 -0
  6. package/dist/app/assets/PackagePage-BbnhGoD3.js +1 -0
  7. package/dist/app/assets/{RouteError-DefbDO7F.js → RouteError-D3LGEZ3i.js} +1 -1
  8. package/dist/app/assets/WorkbookPage-DttVIj4u.js +1 -0
  9. package/dist/app/assets/{core-BrfQApxh.es-DnvCX4oH.js → core-w79IMXAG.es-Bd0UlzOL.js} +1 -1
  10. package/dist/app/assets/{index-Bu0ub036.js → index-5K9YjIxF.js} +117 -117
  11. package/dist/app/assets/{index-CkzK3JIl.js → index-C513UodQ.js} +1 -1
  12. package/dist/app/assets/{index-CoA6HIGS.js → index-DIgzgp69.js} +1 -1
  13. package/dist/app/assets/{index.umd-B6Ms2PpL.js → index.umd-BMeMPq_9.js} +1 -1
  14. package/dist/app/index.html +1 -1
  15. package/dist/server.mjs +1954 -1318
  16. package/package.json +1 -1
  17. package/publisher.config.json +2 -2
  18. package/src/config.spec.ts +181 -66
  19. package/src/config.ts +68 -47
  20. package/src/controller/compile.controller.ts +10 -7
  21. package/src/controller/connection.controller.ts +79 -58
  22. package/src/controller/database.controller.ts +10 -7
  23. package/src/controller/manifest.controller.ts +23 -14
  24. package/src/controller/materialization.controller.ts +14 -14
  25. package/src/controller/model.controller.ts +35 -20
  26. package/src/controller/package.controller.ts +83 -49
  27. package/src/controller/query.controller.ts +11 -8
  28. package/src/controller/watch-mode.controller.ts +35 -29
  29. package/src/errors.ts +2 -2
  30. package/src/mcp/error_messages.ts +2 -2
  31. package/src/mcp/handler_utils.ts +23 -20
  32. package/src/mcp/mcp_constants.ts +1 -1
  33. package/src/mcp/prompts/handlers.ts +3 -3
  34. package/src/mcp/prompts/prompt_service.ts +5 -5
  35. package/src/mcp/prompts/utils.ts +12 -12
  36. package/src/mcp/resource_metadata.ts +3 -3
  37. package/src/mcp/resources/environment_resource.ts +187 -0
  38. package/src/mcp/resources/model_resource.ts +19 -17
  39. package/src/mcp/resources/notebook_resource.ts +13 -13
  40. package/src/mcp/resources/package_resource.ts +30 -27
  41. package/src/mcp/resources/query_resource.ts +15 -10
  42. package/src/mcp/resources/source_resource.ts +10 -10
  43. package/src/mcp/resources/view_resource.ts +11 -11
  44. package/src/mcp/server.ts +16 -14
  45. package/src/mcp/tools/discovery_tools.ts +67 -49
  46. package/src/mcp/tools/execute_query_tool.ts +14 -14
  47. package/src/server-old.ts +1119 -0
  48. package/src/server.ts +191 -159
  49. package/src/service/connection.spec.ts +158 -133
  50. package/src/service/connection.ts +42 -39
  51. package/src/service/connection_config.spec.ts +13 -11
  52. package/src/service/connection_config.ts +28 -19
  53. package/src/service/connection_service.spec.ts +63 -43
  54. package/src/service/connection_service.ts +106 -89
  55. package/src/service/{project.ts → environment.ts} +92 -77
  56. package/src/service/{project_compile.spec.ts → environment_compile.spec.ts} +1 -1
  57. package/src/service/{project_store.spec.ts → environment_store.spec.ts} +99 -85
  58. package/src/service/{project_store.ts → environment_store.ts} +368 -326
  59. package/src/service/manifest_service.spec.ts +15 -15
  60. package/src/service/manifest_service.ts +26 -21
  61. package/src/service/materialization_service.spec.ts +93 -59
  62. package/src/service/materialization_service.ts +71 -62
  63. package/src/service/materialized_table_gc.spec.ts +15 -15
  64. package/src/service/materialized_table_gc.ts +3 -3
  65. package/src/service/model.ts +2 -2
  66. package/src/service/package.spec.ts +2 -2
  67. package/src/service/package.ts +23 -21
  68. package/src/service/resolve_environment.ts +15 -0
  69. package/src/storage/DatabaseInterface.ts +34 -25
  70. package/src/storage/StorageManager.mock.ts +3 -3
  71. package/src/storage/StorageManager.ts +24 -23
  72. package/src/storage/duckdb/ConnectionRepository.ts +13 -11
  73. package/src/storage/duckdb/DuckDBConnection.ts +1 -1
  74. package/src/storage/duckdb/DuckDBManifestStore.ts +6 -6
  75. package/src/storage/duckdb/DuckDBRepository.ts +47 -47
  76. package/src/storage/duckdb/{ProjectRepository.ts → EnvironmentRepository.ts} +35 -35
  77. package/src/storage/duckdb/ManifestRepository.ts +21 -20
  78. package/src/storage/duckdb/MaterializationRepository.ts +31 -28
  79. package/src/storage/duckdb/PackageRepository.ts +11 -11
  80. package/src/storage/duckdb/manifest_store.spec.ts +2 -2
  81. package/src/storage/duckdb/schema.ts +61 -20
  82. package/src/storage/ducklake/DuckLakeManifestStore.ts +14 -14
  83. package/tests/fixtures/publisher.config.json +1 -1
  84. package/tests/harness/e2e.ts +1 -1
  85. package/tests/harness/mcp_test_setup.ts +1 -1
  86. package/tests/harness/mocks.ts +10 -8
  87. package/tests/harness/rest_e2e.ts +2 -2
  88. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +259 -0
  89. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +4 -4
  90. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +27 -48
  91. package/tests/integration/mcp/mcp_resource.integration.spec.ts +26 -35
  92. package/tests/unit/duckdb/attached_databases.test.ts +51 -33
  93. package/tests/unit/duckdb/legacy_schema_migration.test.ts +194 -0
  94. package/tests/unit/ducklake/ducklake.test.ts +24 -22
  95. package/tests/unit/mcp/prompt_happy.test.ts +8 -8
  96. package/dist/app/assets/HomePage-DbZS0N7G.js +0 -1
  97. package/dist/app/assets/MainPage-CBuWkbmr.js +0 -2
  98. package/dist/app/assets/ModelPage-Bt37smot.js +0 -1
  99. package/dist/app/assets/PackagePage-DLZe50WG.js +0 -1
  100. package/dist/app/assets/ProjectPage-FQTEPXP4.js +0 -1
  101. package/dist/app/assets/WorkbookPage-CkAo16ar.js +0 -1
  102. package/src/mcp/resources/project_resource.ts +0 -184
  103. package/src/service/resolve_project.ts +0 -13
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.196-dev",
4
+ "version": "0.0.196",
5
5
  "main": "dist/server.mjs",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.mjs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "frozenConfig": false,
3
- "projects": [
3
+ "environments": [
4
4
  {
5
5
  "name": "malloy-samples",
6
6
  "packages": [
@@ -30,4 +30,4 @@
30
30
  ]
31
31
  }
32
32
  ]
33
- }
33
+ }
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { getPublisherConfig, type PublisherConfig } from "./config";
@@ -66,7 +66,7 @@ describe("Config Environment Variable Substitution", () => {
66
66
 
67
67
  const config: PublisherConfig = {
68
68
  frozenConfig: false,
69
- projects: [
69
+ environments: [
70
70
  {
71
71
  name: "test-project",
72
72
  packages: [
@@ -92,7 +92,7 @@ describe("Config Environment Variable Substitution", () => {
92
92
 
93
93
  const config: PublisherConfig = {
94
94
  frozenConfig: false,
95
- projects: [
95
+ environments: [
96
96
  {
97
97
  name: "test-project",
98
98
  packages: [
@@ -120,7 +120,7 @@ describe("Config Environment Variable Substitution", () => {
120
120
 
121
121
  const config: PublisherConfig = {
122
122
  frozenConfig: false,
123
- projects: [
123
+ environments: [
124
124
  {
125
125
  name: "test-project",
126
126
  packages: [
@@ -137,7 +137,7 @@ describe("Config Environment Variable Substitution", () => {
137
137
 
138
138
  const result = getPublisherConfig(testServerRoot);
139
139
 
140
- expect(result.projects[0].packages[0].location).toBe(
140
+ expect(result.environments[0].packages[0].location).toBe(
141
141
  "gs://my-test-bucket/packages",
142
142
  );
143
143
  });
@@ -147,13 +147,13 @@ describe("Config Environment Variable Substitution", () => {
147
147
 
148
148
  const config: PublisherConfig = {
149
149
  frozenConfig: false,
150
- projects: [
150
+ environments: [
151
151
  {
152
152
  name: "test-project",
153
153
  packages: [
154
154
  {
155
155
  name: "test-package",
156
- location: "./projects/${PROJECT_ID}/models",
156
+ location: "./environments/${PROJECT_ID}/models",
157
157
  },
158
158
  ],
159
159
  },
@@ -164,8 +164,8 @@ describe("Config Environment Variable Substitution", () => {
164
164
 
165
165
  const result = getPublisherConfig(testServerRoot);
166
166
 
167
- expect(result.projects[0].packages[0].location).toBe(
168
- "./projects/analytics-2024/models",
167
+ expect(result.environments[0].packages[0].location).toBe(
168
+ "./environments/analytics-2024/models",
169
169
  );
170
170
  });
171
171
 
@@ -175,7 +175,7 @@ describe("Config Environment Variable Substitution", () => {
175
175
 
176
176
  const config: PublisherConfig = {
177
177
  frozenConfig: false,
178
- projects: [
178
+ environments: [
179
179
  {
180
180
  name: "test-project",
181
181
  packages: [
@@ -192,7 +192,7 @@ describe("Config Environment Variable Substitution", () => {
192
192
 
193
193
  const result = getPublisherConfig(testServerRoot);
194
194
 
195
- expect(result.projects[0].packages[0].location).toBe(
195
+ expect(result.environments[0].packages[0].location).toBe(
196
196
  "gs://data-warehouse/prod-analytics/models",
197
197
  );
198
198
  });
@@ -203,7 +203,7 @@ describe("Config Environment Variable Substitution", () => {
203
203
 
204
204
  const config: PublisherConfig = {
205
205
  frozenConfig: false,
206
- projects: [
206
+ environments: [
207
207
  {
208
208
  name: "test-project",
209
209
  packages: [
@@ -224,10 +224,10 @@ describe("Config Environment Variable Substitution", () => {
224
224
 
225
225
  const result = getPublisherConfig(testServerRoot);
226
226
 
227
- expect(result.projects[0].packages[0].location).toBe(
227
+ expect(result.environments[0].packages[0].location).toBe(
228
228
  "gs://bucket-one/path",
229
229
  );
230
- expect(result.projects[0].packages[1].location).toBe(
230
+ expect(result.environments[0].packages[1].location).toBe(
231
231
  "gs://bucket-two/path",
232
232
  );
233
233
  });
@@ -237,7 +237,7 @@ describe("Config Environment Variable Substitution", () => {
237
237
 
238
238
  const config: PublisherConfig = {
239
239
  frozenConfig: false,
240
- projects: [
240
+ environments: [
241
241
  {
242
242
  name: "test-project",
243
243
  packages: [],
@@ -255,7 +255,9 @@ describe("Config Environment Variable Substitution", () => {
255
255
 
256
256
  const result = getPublisherConfig(testServerRoot);
257
257
 
258
- expect(result.projects[0].connections?.[0].name).toBe("db-localhost");
258
+ expect(result.environments[0].connections?.[0].name).toBe(
259
+ "db-localhost",
260
+ );
259
261
  });
260
262
 
261
263
  it("should substitute variables in nested configuration objects", () => {
@@ -264,7 +266,7 @@ describe("Config Environment Variable Substitution", () => {
264
266
 
265
267
  const config = {
266
268
  frozenConfig: false,
267
- projects: [
269
+ environments: [
268
270
  {
269
271
  name: "test-project",
270
272
  packages: [],
@@ -282,7 +284,7 @@ describe("Config Environment Variable Substitution", () => {
282
284
 
283
285
  const result = getPublisherConfig(testServerRoot);
284
286
  const projectWithSettings = result
285
- .projects[0] as (typeof result.projects)[0] & {
287
+ .environments[0] as (typeof result.environments)[0] & {
286
288
  settings: {
287
289
  apiEndpoint: string;
288
290
  credentials: {
@@ -304,7 +306,7 @@ describe("Config Environment Variable Substitution", () => {
304
306
 
305
307
  const config: PublisherConfig = {
306
308
  frozenConfig: false,
307
- projects: [
309
+ environments: [
308
310
  {
309
311
  name: "test-project",
310
312
  packages: [
@@ -322,18 +324,18 @@ describe("Config Environment Variable Substitution", () => {
322
324
 
323
325
  const result = getPublisherConfig(testServerRoot);
324
326
 
325
- expect(result.projects[0].packages[0].location).toBe(
327
+ expect(result.environments[0].packages[0].location).toBe(
326
328
  "gs://bucket/prefix-my-project-suffix/models",
327
329
  );
328
330
  });
329
331
 
330
- it("should substitute variables across multiple projects", () => {
332
+ it("should substitute variables across multiple environments", () => {
331
333
  process.env.DEV_BUCKET = "dev-data";
332
334
  process.env.PROD_BUCKET = "prod-data";
333
335
 
334
336
  const config: PublisherConfig = {
335
337
  frozenConfig: false,
336
- projects: [
338
+ environments: [
337
339
  {
338
340
  name: "development",
339
341
  packages: [
@@ -359,10 +361,10 @@ describe("Config Environment Variable Substitution", () => {
359
361
 
360
362
  const result = getPublisherConfig(testServerRoot);
361
363
 
362
- expect(result.projects[0].packages[0].location).toBe(
364
+ expect(result.environments[0].packages[0].location).toBe(
363
365
  "gs://dev-data/models",
364
366
  );
365
- expect(result.projects[1].packages[0].location).toBe(
367
+ expect(result.environments[1].packages[0].location).toBe(
366
368
  "gs://prod-data/models",
367
369
  );
368
370
 
@@ -377,7 +379,7 @@ describe("Config Environment Variable Substitution", () => {
377
379
 
378
380
  const config = {
379
381
  frozenConfig: false,
380
- projects: [
382
+ environments: [
381
383
  {
382
384
  name: "test-project",
383
385
  packages: [],
@@ -391,9 +393,9 @@ describe("Config Environment Variable Substitution", () => {
391
393
  const result = getPublisherConfig(testServerRoot);
392
394
 
393
395
  // The key should remain as-is (not substituted)
394
- expect(result.projects[0]).toHaveProperty("${KEY_NAME}");
396
+ expect(result.environments[0]).toHaveProperty("${KEY_NAME}");
395
397
  expect(
396
- (result.projects[0] as Record<string, unknown>)["${KEY_NAME}"],
398
+ (result.environments[0] as Record<string, unknown>)["${KEY_NAME}"],
397
399
  ).toBe("some-value");
398
400
  });
399
401
 
@@ -402,7 +404,7 @@ describe("Config Environment Variable Substitution", () => {
402
404
 
403
405
  const config = {
404
406
  frozenConfig: false,
405
- projects: [
407
+ environments: [
406
408
  {
407
409
  name: "test-project",
408
410
  packages: [],
@@ -417,14 +419,16 @@ describe("Config Environment Variable Substitution", () => {
417
419
  const result = getPublisherConfig(testServerRoot);
418
420
 
419
421
  // Key should not be substituted
420
- expect(result.projects[0]).toHaveProperty("${DYNAMIC_KEY}");
422
+ expect(result.environments[0]).toHaveProperty("${DYNAMIC_KEY}");
421
423
  expect(
422
- (result.projects[0] as Record<string, unknown>)["${DYNAMIC_KEY}"],
424
+ (result.environments[0] as Record<string, unknown>)[
425
+ "${DYNAMIC_KEY}"
426
+ ],
423
427
  ).toBe("value1");
424
428
 
425
429
  // Value should be substituted
426
430
  expect(
427
- (result.projects[0] as Record<string, unknown>)["normal_key"],
431
+ (result.environments[0] as Record<string, unknown>)["normal_key"],
428
432
  ).toBe("substituted-value");
429
433
  });
430
434
 
@@ -433,7 +437,7 @@ describe("Config Environment Variable Substitution", () => {
433
437
 
434
438
  const config = {
435
439
  frozenConfig: false,
436
- projects: [
440
+ environments: [
437
441
  {
438
442
  name: "test-project",
439
443
  packages: [],
@@ -447,11 +451,11 @@ describe("Config Environment Variable Substitution", () => {
447
451
  const result = getPublisherConfig(testServerRoot);
448
452
 
449
453
  // Key remains with variable syntax
450
- expect(result.projects[0]).toHaveProperty("${KEY_VAR}");
454
+ expect(result.environments[0]).toHaveProperty("${KEY_VAR}");
451
455
 
452
456
  // Value is substituted
453
457
  expect(
454
- (result.projects[0] as Record<string, unknown>)["${KEY_VAR}"],
458
+ (result.environments[0] as Record<string, unknown>)["${KEY_VAR}"],
455
459
  ).toBe("actual-value");
456
460
  });
457
461
 
@@ -461,7 +465,7 @@ describe("Config Environment Variable Substitution", () => {
461
465
 
462
466
  const config: PublisherConfig = {
463
467
  frozenConfig: false,
464
- projects: [
468
+ environments: [
465
469
  {
466
470
  name: "test-project",
467
471
  packages: [
@@ -479,10 +483,10 @@ describe("Config Environment Variable Substitution", () => {
479
483
  const result = getPublisherConfig(testServerRoot);
480
484
 
481
485
  // Package name is a property VALUE, so it WILL be substituted
482
- expect(result.projects[0].packages[0].name).toBe("my-package");
486
+ expect(result.environments[0].packages[0].name).toBe("my-package");
483
487
 
484
488
  // Location should also be substituted
485
- expect(result.projects[0].packages[0].location).toBe(
489
+ expect(result.environments[0].packages[0].location).toBe(
486
490
  "gs://my-bucket/models",
487
491
  );
488
492
 
@@ -494,7 +498,7 @@ describe("Config Environment Variable Substitution", () => {
494
498
 
495
499
  const config = {
496
500
  frozenConfig: false,
497
- projects: [
501
+ environments: [
498
502
  {
499
503
  name: "test-project",
500
504
  packages: [],
@@ -511,7 +515,7 @@ describe("Config Environment Variable Substitution", () => {
511
515
 
512
516
  const result = getPublisherConfig(testServerRoot);
513
517
  const projectWithMetadata = result
514
- .projects[0] as (typeof result.projects)[0] & {
518
+ .environments[0] as (typeof result.environments)[0] & {
515
519
  metadata: Record<string, { setting: string }>;
516
520
  };
517
521
 
@@ -533,7 +537,7 @@ describe("Config Environment Variable Substitution", () => {
533
537
 
534
538
  const config: PublisherConfig = {
535
539
  frozenConfig: false,
536
- projects: [
540
+ environments: [
537
541
  {
538
542
  name: "test-project",
539
543
  packages: [
@@ -550,7 +554,7 @@ describe("Config Environment Variable Substitution", () => {
550
554
 
551
555
  const result = getPublisherConfig(testServerRoot);
552
556
 
553
- expect(result.projects[0].packages[0].location).toBe(
557
+ expect(result.environments[0].packages[0].location).toBe(
554
558
  "gs://bucket//path",
555
559
  );
556
560
  });
@@ -558,7 +562,7 @@ describe("Config Environment Variable Substitution", () => {
558
562
  it("should handle non-string values without modification", () => {
559
563
  const config: PublisherConfig = {
560
564
  frozenConfig: true,
561
- projects: [
565
+ environments: [
562
566
  {
563
567
  name: "test-project",
564
568
  packages: [],
@@ -569,9 +573,9 @@ describe("Config Environment Variable Substitution", () => {
569
573
  // Add non-standard properties for testing
570
574
  const configWithExtras = {
571
575
  ...config,
572
- projects: [
576
+ environments: [
573
577
  {
574
- ...config.projects[0],
578
+ ...config.environments[0],
575
579
  count: 42,
576
580
  enabled: true,
577
581
  ratio: 3.14,
@@ -588,7 +592,7 @@ describe("Config Environment Variable Substitution", () => {
588
592
 
589
593
  const result = getPublisherConfig(testServerRoot);
590
594
  const projectWithExtras = result
591
- .projects[0] as (typeof result.projects)[0] & {
595
+ .environments[0] as (typeof result.environments)[0] & {
592
596
  count: number;
593
597
  enabled: boolean;
594
598
  ratio: number;
@@ -607,7 +611,7 @@ describe("Config Environment Variable Substitution", () => {
607
611
  it("should handle config with no environment variables", () => {
608
612
  const config: PublisherConfig = {
609
613
  frozenConfig: false,
610
- projects: [
614
+ environments: [
611
615
  {
612
616
  name: "test-project",
613
617
  packages: [
@@ -627,17 +631,17 @@ describe("Config Environment Variable Substitution", () => {
627
631
  expect(result).toEqual(config);
628
632
  });
629
633
 
630
- it("should handle empty projects array", () => {
634
+ it("should handle empty environments array", () => {
631
635
  const config: PublisherConfig = {
632
636
  frozenConfig: false,
633
- projects: [],
637
+ environments: [],
634
638
  };
635
639
 
636
640
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
637
641
 
638
642
  const result = getPublisherConfig(testServerRoot);
639
643
 
640
- expect(result.projects).toEqual([]);
644
+ expect(result.environments).toEqual([]);
641
645
  });
642
646
 
643
647
  it("should return default config when file does not exist", () => {
@@ -646,7 +650,7 @@ describe("Config Environment Variable Substitution", () => {
646
650
 
647
651
  expect(result).toEqual({
648
652
  frozenConfig: false,
649
- projects: [],
653
+ environments: [],
650
654
  });
651
655
  });
652
656
 
@@ -655,7 +659,7 @@ describe("Config Environment Variable Substitution", () => {
655
659
 
656
660
  const config: PublisherConfig = {
657
661
  frozenConfig: false,
658
- projects: [
662
+ environments: [
659
663
  {
660
664
  name: "test-project",
661
665
  packages: [
@@ -674,17 +678,17 @@ describe("Config Environment Variable Substitution", () => {
674
678
 
675
679
  // Whitespace around variable name means it won't match the pattern
676
680
  // Due to bug, the unmatched variable causes duplication
677
- expect(result.projects[0].packages[0].location).toBe(
681
+ expect(result.environments[0].packages[0].location).toBe(
678
682
  "gs://${ BUCKET_NAME }/path",
679
683
  );
680
684
  });
681
685
 
682
- it("should handle multiple projects with mixed variable usage", () => {
686
+ it("should handle multiple environments with mixed variable usage", () => {
683
687
  process.env.PROD_BUCKET = "production-data";
684
688
 
685
689
  const config: PublisherConfig = {
686
690
  frozenConfig: false,
687
- projects: [
691
+ environments: [
688
692
  {
689
693
  name: "development",
690
694
  packages: [
@@ -710,8 +714,10 @@ describe("Config Environment Variable Substitution", () => {
710
714
 
711
715
  const result = getPublisherConfig(testServerRoot);
712
716
 
713
- expect(result.projects[0].packages[0].location).toBe("./packages/dev");
714
- expect(result.projects[1].packages[0].location).toBe(
717
+ expect(result.environments[0].packages[0].location).toBe(
718
+ "./packages/dev",
719
+ );
720
+ expect(result.environments[1].packages[0].location).toBe(
715
721
  "gs://production-data/models",
716
722
  );
717
723
 
@@ -721,7 +727,7 @@ describe("Config Environment Variable Substitution", () => {
721
727
  it("should preserve variable syntax if not matching pattern", () => {
722
728
  const config: PublisherConfig = {
723
729
  frozenConfig: false,
724
- projects: [
730
+ environments: [
725
731
  {
726
732
  name: "test-project",
727
733
  packages: [
@@ -739,7 +745,7 @@ describe("Config Environment Variable Substitution", () => {
739
745
  const result = getPublisherConfig(testServerRoot);
740
746
 
741
747
  // Variable won't match pattern, so won't be substituted
742
- expect(result.projects[0].packages[0].location).toBe(
748
+ expect(result.environments[0].packages[0].location).toBe(
743
749
  "gs://bucket/${lowercase_var}/path",
744
750
  );
745
751
  });
@@ -750,7 +756,7 @@ describe("Config Environment Variable Substitution", () => {
750
756
 
751
757
  const config = {
752
758
  frozenConfig: false,
753
- projects: [
759
+ environments: [
754
760
  {
755
761
  name: "test-project",
756
762
  packages: [],
@@ -763,7 +769,7 @@ describe("Config Environment Variable Substitution", () => {
763
769
 
764
770
  const result = getPublisherConfig(testServerRoot);
765
771
  const projectWithTags = result
766
- .projects[0] as (typeof result.projects)[0] & {
772
+ .environments[0] as (typeof result.environments)[0] & {
767
773
  tags: string[];
768
774
  };
769
775
 
@@ -786,7 +792,7 @@ describe("Config Environment Variable Substitution", () => {
786
792
 
787
793
  const config: PublisherConfig = {
788
794
  frozenConfig: false,
789
- projects: [
795
+ environments: [
790
796
  {
791
797
  name: "${ENV}",
792
798
  packages: [
@@ -807,11 +813,11 @@ describe("Config Environment Variable Substitution", () => {
807
813
 
808
814
  const result = getPublisherConfig(testServerRoot);
809
815
 
810
- expect(result.projects[0].name).toBe("staging");
811
- expect(result.projects[0].packages[0].location).toBe(
816
+ expect(result.environments[0].name).toBe("staging");
817
+ expect(result.environments[0].packages[0].location).toBe(
812
818
  "gs://company-data-staging/company-project-staging/analytics",
813
819
  );
814
- expect(result.projects[0].packages[1].location).toBe(
820
+ expect(result.environments[0].packages[1].location).toBe(
815
821
  "gs://company-data-staging/company-project-staging/reporting",
816
822
  );
817
823
 
@@ -824,7 +830,7 @@ describe("Config Environment Variable Substitution", () => {
824
830
 
825
831
  const config: PublisherConfig = {
826
832
  frozenConfig: false,
827
- projects: [
833
+ environments: [
828
834
  {
829
835
  name: "production",
830
836
  packages: [],
@@ -842,9 +848,118 @@ describe("Config Environment Variable Substitution", () => {
842
848
 
843
849
  const result = getPublisherConfig(testServerRoot);
844
850
 
845
- expect(result.projects[0].connections?.[0].name).toBe("production-db");
851
+ expect(result.environments[0].connections?.[0].name).toBe(
852
+ "production-db",
853
+ );
846
854
 
847
855
  delete process.env.DB_CONNECTION;
848
856
  });
849
857
  });
850
858
  });
859
+
860
+ // TODO: Remove this during projects cleanup
861
+ describe("Config legacy 'projects' key back-compat", () => {
862
+ const testServerRoot = path.join(process.cwd(), "test-temp-legacy-config");
863
+ const configPath = path.join(testServerRoot, PUBLISHER_CONFIG_NAME);
864
+
865
+ beforeEach(() => {
866
+ if (!fs.existsSync(testServerRoot)) {
867
+ fs.mkdirSync(testServerRoot, { recursive: true });
868
+ }
869
+ });
870
+
871
+ afterEach(() => {
872
+ if (fs.existsSync(configPath)) {
873
+ fs.unlinkSync(configPath);
874
+ }
875
+ if (fs.existsSync(testServerRoot)) {
876
+ fs.rmdirSync(testServerRoot, { recursive: true });
877
+ }
878
+ });
879
+
880
+ it("reads from legacy 'projects' key when 'environments' is absent", async () => {
881
+ // Pre-rename on-disk shape: top-level key is `projects`, not
882
+ // `environments`. Without back-compat this silently parses as empty.
883
+ const legacyConfig = {
884
+ frozenConfig: false,
885
+ projects: [
886
+ {
887
+ name: "legacy-env",
888
+ packages: [
889
+ {
890
+ name: "p1",
891
+ location: "./packages/p1",
892
+ },
893
+ ],
894
+ },
895
+ ],
896
+ };
897
+
898
+ fs.writeFileSync(configPath, JSON.stringify(legacyConfig, null, 2));
899
+
900
+ // Spy on logger.warn so we can assert the deprecation message fired.
901
+ const { logger } = await import("./logger");
902
+ const originalWarn = logger.warn;
903
+ const warnings: string[] = [];
904
+ logger.warn = ((msg: unknown, ..._rest: unknown[]) => {
905
+ warnings.push(typeof msg === "string" ? msg : String(msg));
906
+ return logger;
907
+ }) as typeof logger.warn;
908
+
909
+ try {
910
+ const result = getPublisherConfig(testServerRoot);
911
+
912
+ expect(result.environments.length).toBe(1);
913
+ expect(result.environments[0].name).toBe("legacy-env");
914
+ expect(result.environments[0].packages[0].name).toBe("p1");
915
+
916
+ expect(
917
+ warnings.some((w) => w.includes('uses deprecated "projects" key')),
918
+ ).toBe(true);
919
+ } finally {
920
+ logger.warn = originalWarn;
921
+ }
922
+ });
923
+
924
+ it("prefers the new 'environments' key when both are present", async () => {
925
+ const config = {
926
+ frozenConfig: false,
927
+ environments: [
928
+ {
929
+ name: "new-env",
930
+ packages: [{ name: "p1", location: "./packages/p1" }],
931
+ },
932
+ ],
933
+ projects: [
934
+ {
935
+ name: "should-be-ignored",
936
+ packages: [{ name: "p2", location: "./packages/p2" }],
937
+ },
938
+ ],
939
+ };
940
+
941
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
942
+
943
+ const { logger } = await import("./logger");
944
+ const originalWarn = logger.warn;
945
+ const warnings: string[] = [];
946
+ logger.warn = ((msg: unknown, ..._rest: unknown[]) => {
947
+ warnings.push(typeof msg === "string" ? msg : String(msg));
948
+ return logger;
949
+ }) as typeof logger.warn;
950
+
951
+ try {
952
+ const result = getPublisherConfig(testServerRoot);
953
+
954
+ expect(result.environments.length).toBe(1);
955
+ expect(result.environments[0].name).toBe("new-env");
956
+
957
+ // No deprecation warning should fire when `environments` is present.
958
+ expect(
959
+ warnings.some((w) => w.includes('uses deprecated "projects" key')),
960
+ ).toBe(false);
961
+ } finally {
962
+ logger.warn = originalWarn;
963
+ }
964
+ });
965
+ });