@malloy-publisher/server 0.0.191 → 0.0.193

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 (34) hide show
  1. package/dist/app/api-doc.yaml +531 -3
  2. package/dist/app/assets/{HomePage-Dn3E4CuB.js → HomePage-Di9MU3lS.js} +1 -1
  3. package/dist/app/assets/{MainPage-BzB3yoqi.js → MainPage-yZQo2HSL.js} +1 -1
  4. package/dist/app/assets/{ModelPage-C9O_sAXT.js → ModelPage-Dx2mHWeT.js} +1 -1
  5. package/dist/app/assets/{PackagePage-DcxKEjBX.js → PackagePage-Q386Py9t.js} +1 -1
  6. package/dist/app/assets/{ProjectPage-BDj307rF.js → ProjectPage-WR7wPQB-.js} +1 -1
  7. package/dist/app/assets/{RouteError-DAShbVCG.js → RouteError-stRGU4aW.js} +1 -1
  8. package/dist/app/assets/{WorkbookPage-Cs_XYEaB.js → WorkbookPage-D3iX0djH.js} +1 -1
  9. package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-QH4HZQVz.es-CqlQLZdl.js} +1 -1
  10. package/dist/app/assets/{index-15BOvhp0.js → index-CVHzPJwN.js} +119 -119
  11. package/dist/app/assets/{index-D68X76-7.js → index-DavAceYD.js} +50 -50
  12. package/dist/app/assets/{index-Bb2jqquW.js → index-Y3Y-VRna.js} +1 -1
  13. package/dist/app/assets/{index.umd-DGBekgSu.js → index.umd-Bp8OIhfV.js} +46 -46
  14. package/dist/app/index.html +1 -1
  15. package/dist/server.mjs +1396 -985
  16. package/package.json +10 -10
  17. package/src/controller/connection.controller.ts +102 -27
  18. package/src/dto/connection.dto.spec.ts +4 -0
  19. package/src/dto/connection.dto.ts +46 -2
  20. package/src/server.ts +217 -9
  21. package/src/service/connection.spec.ts +250 -4
  22. package/src/service/connection.ts +326 -473
  23. package/src/service/connection_config.ts +514 -0
  24. package/src/service/connection_service.spec.ts +50 -0
  25. package/src/service/connection_service.ts +125 -32
  26. package/src/service/materialization_service.spec.ts +18 -12
  27. package/src/service/materialization_service.ts +54 -7
  28. package/src/service/model.ts +24 -27
  29. package/src/service/package.spec.ts +125 -1
  30. package/src/service/package.ts +86 -44
  31. package/src/service/project.ts +172 -94
  32. package/src/service/project_store.spec.ts +72 -0
  33. package/src/service/project_store.ts +98 -81
  34. package/tests/unit/duckdb/attached_databases.test.ts +1 -19
package/src/server.ts CHANGED
@@ -1,10 +1,4 @@
1
1
  // Pre-load the instrumentation module; the instrumentation module must be loaded before the other imports.
2
- import "./instrumentation";
3
- import {
4
- getPrometheusMetricsHandler,
5
- httpMetricsMiddleware,
6
- } from "./instrumentation";
7
-
8
2
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
3
  import bodyParser from "body-parser";
10
4
  import cors from "cors";
@@ -13,6 +7,7 @@ import * as http from "http";
13
7
  import { createProxyMiddleware } from "http-proxy-middleware";
14
8
  import { AddressInfo } from "net";
15
9
  import * as path from "path";
10
+ import { ParsedQs } from "qs";
16
11
  import { fileURLToPath } from "url";
17
12
  import { CompileController } from "./controller/compile.controller";
18
13
  import { ConnectionController } from "./controller/connection.controller";
@@ -31,6 +26,11 @@ import {
31
26
  registerHealthEndpoints,
32
27
  registerSignalHandlers,
33
28
  } from "./health";
29
+ import "./instrumentation";
30
+ import {
31
+ getPrometheusMetricsHandler,
32
+ httpMetricsMiddleware,
33
+ } from "./instrumentation";
34
34
  import { logger, loggerMiddleware } from "./logger";
35
35
 
36
36
  import { ManifestController } from "./controller/manifest.controller";
@@ -524,6 +524,70 @@ app.get(
524
524
  },
525
525
  );
526
526
 
527
+ // ── Per-package connection data routes ─────────────────────────────
528
+ // `duckdb` is per-package; non-`duckdb` names fall through to the
529
+ // project's connection registry via the package's MalloyConfig wrapper.
530
+ app.get(
531
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/schemas`,
532
+ async (req, res) => {
533
+ try {
534
+ res.status(200).json(
535
+ await connectionController.listSchemas(
536
+ req.params.projectName,
537
+ req.params.connectionName,
538
+ req.params.packageName,
539
+ ),
540
+ );
541
+ } catch (error) {
542
+ logger.error(error);
543
+ const { json, status } = internalErrorToHttpError(error as Error);
544
+ res.status(status).json(json);
545
+ }
546
+ },
547
+ );
548
+
549
+ app.get(
550
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/schemas/:schemaName/tables`,
551
+ async (req, res) => {
552
+ try {
553
+ res.status(200).json(
554
+ await connectionController.listTables(
555
+ req.params.projectName,
556
+ req.params.connectionName,
557
+ req.params.schemaName,
558
+ normalizeQueryArray(req.query.tableNames),
559
+ req.params.packageName,
560
+ ),
561
+ );
562
+ } catch (error) {
563
+ logger.error(error);
564
+ const { json, status } = internalErrorToHttpError(error as Error);
565
+ res.status(status).json(json);
566
+ }
567
+ },
568
+ );
569
+
570
+ app.get(
571
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/schemas/:schemaName/tables/:tablePath`,
572
+ async (req, res) => {
573
+ try {
574
+ res.status(200).json(
575
+ await connectionController.getTable(
576
+ req.params.projectName,
577
+ req.params.connectionName,
578
+ req.params.schemaName,
579
+ req.params.tablePath,
580
+ req.params.packageName,
581
+ ),
582
+ );
583
+ } catch (error) {
584
+ logger.error(error);
585
+ const { json, status } = internalErrorToHttpError(error as Error);
586
+ res.status(status).json(json);
587
+ }
588
+ },
589
+ );
590
+
527
591
  /**
528
592
  * @deprecated Use /projects/:projectName/connections/:connectionName/sqlSource POST method instead
529
593
  */
@@ -565,8 +629,49 @@ app.post(
565
629
  },
566
630
  );
567
631
 
632
+ // Per-package versions
633
+ app.get(
634
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlSource`,
635
+ async (req, res) => {
636
+ try {
637
+ res.status(200).json(
638
+ await connectionController.getConnectionSqlSource(
639
+ req.params.projectName,
640
+ req.params.connectionName,
641
+ req.query.sqlStatement as string,
642
+ req.params.packageName,
643
+ ),
644
+ );
645
+ } catch (error) {
646
+ logger.error(error);
647
+ const { json, status } = internalErrorToHttpError(error as Error);
648
+ res.status(status).json(json);
649
+ }
650
+ },
651
+ );
652
+
653
+ app.post(
654
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlSource`,
655
+ async (req, res) => {
656
+ try {
657
+ res.status(200).json(
658
+ await connectionController.getConnectionSqlSource(
659
+ req.params.projectName,
660
+ req.params.connectionName,
661
+ req.body.sqlStatement as string,
662
+ req.params.packageName,
663
+ ),
664
+ );
665
+ } catch (error) {
666
+ logger.error(error);
667
+ const { json, status } = internalErrorToHttpError(error as Error);
668
+ res.status(status).json(json);
669
+ }
670
+ },
671
+ );
672
+
568
673
  /**
569
- * @deprecated Use /projects/:projectName/connections/:connectionName/queryData POST method instead
674
+ * @deprecated Use /projects/:projectName/connections/:connectionName/sqlQuery POST method instead
570
675
  */
571
676
  app.get(
572
677
  `${API_PREFIX}/projects/:projectName/connections/:connectionName/queryData`,
@@ -588,16 +693,76 @@ app.get(
588
693
  },
589
694
  );
590
695
 
696
+ /**
697
+ * @deprecated Use /projects/:projectName/packages/:packageName/connections/:connectionName/sqlQuery
698
+ */
699
+ app.get(
700
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/queryData`,
701
+ async (req, res) => {
702
+ try {
703
+ res.status(200).json(
704
+ await connectionController.getConnectionQueryData(
705
+ req.params.projectName,
706
+ req.params.connectionName,
707
+ req.query.sqlStatement as string,
708
+ req.query.options as string,
709
+ req.params.packageName,
710
+ ),
711
+ );
712
+ } catch (error) {
713
+ logger.error(error);
714
+ const { json, status } = internalErrorToHttpError(error as Error);
715
+ res.status(status).json(json);
716
+ }
717
+ },
718
+ );
719
+
591
720
  app.post(
592
721
  `${API_PREFIX}/projects/:projectName/connections/:connectionName/sqlQuery`,
593
722
  async (req, res) => {
594
723
  try {
724
+ let options: string | ParsedQs | (string | ParsedQs)[] | undefined;
725
+
726
+ // Support both body and query parameters for options for backwards compatibility
727
+ // TODO: To be removed in the future
728
+ if (req.body?.options) {
729
+ options = req.body.options;
730
+ } else {
731
+ options = req.query.options;
732
+ }
595
733
  res.status(200).json(
596
734
  await connectionController.getConnectionQueryData(
597
735
  req.params.projectName,
598
736
  req.params.connectionName,
599
737
  req.body.sqlStatement as string,
600
- req.body.options as string,
738
+ options as string,
739
+ ),
740
+ );
741
+ } catch (error) {
742
+ logger.error(error);
743
+ const { json, status } = internalErrorToHttpError(error as Error);
744
+ res.status(status).json(json);
745
+ }
746
+ },
747
+ );
748
+
749
+ app.post(
750
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlQuery`,
751
+ async (req, res) => {
752
+ try {
753
+ let options: string | ParsedQs | (string | ParsedQs)[] | undefined;
754
+ if (req.body?.options) {
755
+ options = req.body.options;
756
+ } else {
757
+ options = req.query.options;
758
+ }
759
+ res.status(200).json(
760
+ await connectionController.getConnectionQueryData(
761
+ req.params.projectName,
762
+ req.params.connectionName,
763
+ req.body.sqlStatement as string,
764
+ options as string,
765
+ req.params.packageName,
601
766
  ),
602
767
  );
603
768
  } catch (error) {
@@ -609,7 +774,7 @@ app.post(
609
774
  );
610
775
 
611
776
  /**
612
- * @deprecated Use /projects/:projectName/connections/:connectionName/temporaryTable POST method instead
777
+ * @deprecated Use /projects/:projectName/connections/:connectionName/sqlTemporaryTable POST method instead
613
778
  */
614
779
  app.get(
615
780
  `${API_PREFIX}/projects/:projectName/connections/:connectionName/temporaryTable`,
@@ -630,6 +795,29 @@ app.get(
630
795
  },
631
796
  );
632
797
 
798
+ /**
799
+ * @deprecated Use /projects/:projectName/packages/:packageName/connections/:connectionName/sqlTemporaryTable
800
+ */
801
+ app.get(
802
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/temporaryTable`,
803
+ async (req, res) => {
804
+ try {
805
+ res.status(200).json(
806
+ await connectionController.getConnectionTemporaryTable(
807
+ req.params.projectName,
808
+ req.params.connectionName,
809
+ req.query.sqlStatement as string,
810
+ req.params.packageName,
811
+ ),
812
+ );
813
+ } catch (error) {
814
+ logger.error(error);
815
+ const { json, status } = internalErrorToHttpError(error as Error);
816
+ res.status(status).json(json);
817
+ }
818
+ },
819
+ );
820
+
633
821
  app.post(
634
822
  `${API_PREFIX}/projects/:projectName/connections/:connectionName/sqlTemporaryTable`,
635
823
  async (req, res) => {
@@ -649,6 +837,26 @@ app.post(
649
837
  },
650
838
  );
651
839
 
840
+ app.post(
841
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlTemporaryTable`,
842
+ async (req, res) => {
843
+ try {
844
+ res.status(200).json(
845
+ await connectionController.getConnectionTemporaryTable(
846
+ req.params.projectName,
847
+ req.params.connectionName,
848
+ req.body.sqlStatement as string,
849
+ req.params.packageName,
850
+ ),
851
+ );
852
+ } catch (error) {
853
+ logger.error(error);
854
+ const { json, status } = internalErrorToHttpError(error as Error);
855
+ res.status(status).json(json);
856
+ }
857
+ },
858
+ );
859
+
652
860
  app.get(`${API_PREFIX}/projects/:projectName/packages`, async (req, res) => {
653
861
  if (req.query.versionId) {
654
862
  setVersionIdError(res);
@@ -4,6 +4,7 @@ import path from "path";
4
4
  import sinon from "sinon";
5
5
  import { DuckDBConnection } from "@malloydata/db-duckdb";
6
6
  import { createProjectConnections, testConnectionConfig } from "./connection";
7
+ import { assembleProjectConnections } from "./connection_config";
7
8
  import { components } from "../api";
8
9
 
9
10
  type ApiConnection = components["schemas"]["Connection"];
@@ -1121,19 +1122,264 @@ describe("connection integration tests", () => {
1121
1122
  ).rejects.toThrow(/cannot be 'duckdb'/);
1122
1123
  });
1123
1124
 
1124
- it("should throw error if no attached databases configured", async () => {
1125
+ it("should allow DuckDB connections with no attachments", async () => {
1126
+ const { malloyConnections } = await createProjectConnections(
1127
+ [
1128
+ {
1129
+ name: "empty_duckdb",
1130
+ type: "duckdb",
1131
+ duckdbConnection: { attachedDatabases: [] },
1132
+ },
1133
+ ],
1134
+ testProjectPath,
1135
+ );
1136
+
1137
+ const connection = malloyConnections.get("empty_duckdb");
1138
+ expect(connection).toBeDefined();
1139
+ });
1140
+
1141
+ it("should reject unsupported DuckDB connector fields", async () => {
1125
1142
  await expect(
1126
1143
  createProjectConnections(
1127
1144
  [
1128
1145
  {
1129
- name: "empty_duckdb",
1146
+ name: "duckdb_with_setup_sql",
1130
1147
  type: "duckdb",
1131
- duckdbConnection: { attachedDatabases: [] },
1148
+ duckdbConnection: {
1149
+ attachedDatabases: [],
1150
+ setupSQL: "INSTALL httpfs",
1151
+ },
1152
+ } as unknown as ApiConnection,
1153
+ ],
1154
+ testProjectPath,
1155
+ ),
1156
+ ).rejects.toThrow(/Unsupported DuckDB connection field/);
1157
+ });
1158
+
1159
+ it("should reject project-authored DuckDB policy fields", async () => {
1160
+ await expect(
1161
+ createProjectConnections(
1162
+ [
1163
+ {
1164
+ name: "duckdb_with_policy",
1165
+ type: "duckdb",
1166
+ duckdbConnection: {
1167
+ attachedDatabases: [],
1168
+ securityPolicy: "sandboxed",
1169
+ },
1170
+ } as unknown as ApiConnection,
1171
+ ],
1172
+ testProjectPath,
1173
+ ),
1174
+ ).rejects.toThrow(/Unsupported DuckDB connection field/);
1175
+ });
1176
+
1177
+ it("should preserve Snowflake private-key auth options", async () => {
1178
+ const { malloyConnections, releaseConnections } =
1179
+ await createProjectConnections(
1180
+ [
1181
+ {
1182
+ name: "sf_private_key",
1183
+ type: "snowflake",
1184
+ snowflakeConnection: {
1185
+ account: "test-account",
1186
+ username: "test-user",
1187
+ privateKey:
1188
+ "-----BEGIN PRIVATE KEY-----MIIB-----END PRIVATE KEY-----",
1189
+ warehouse: "test-warehouse",
1190
+ },
1191
+ },
1192
+ ],
1193
+ testProjectPath,
1194
+ );
1195
+
1196
+ try {
1197
+ const connection = malloyConnections.get(
1198
+ "sf_private_key",
1199
+ ) as unknown as { connOptions: Record<string, unknown> };
1200
+ expect(connection.connOptions.authenticator).toBe(
1201
+ "SNOWFLAKE_JWT",
1202
+ );
1203
+ expect(connection.connOptions.privateKey).toContain(
1204
+ "BEGIN PRIVATE KEY",
1205
+ );
1206
+ } finally {
1207
+ await releaseConnections();
1208
+ }
1209
+ });
1210
+
1211
+ it("should translate Trino Peaka credentials to core extraCredential", () => {
1212
+ const assembled = assembleProjectConnections(
1213
+ [
1214
+ {
1215
+ name: "trino_peaka",
1216
+ type: "trino",
1217
+ trinoConnection: {
1218
+ server: "https://example.com",
1219
+ port: 443,
1220
+ catalog: "catalog",
1221
+ schema: "schema",
1222
+ user: "user",
1223
+ peakaKey: "peaka-secret",
1224
+ },
1225
+ },
1226
+ ],
1227
+ testProjectPath,
1228
+ );
1229
+
1230
+ expect(
1231
+ assembled.pojo.connections.trino_peaka.extraCredential,
1232
+ ).toEqual({ peakaKey: "peaka-secret" });
1233
+ expect(
1234
+ assembled.pojo.connections.trino_peaka.extraConfig,
1235
+ ).toBeUndefined();
1236
+ });
1237
+
1238
+ it("should validate project-level BigQuery service account keys", () => {
1239
+ expect(() =>
1240
+ assembleProjectConnections(
1241
+ [
1242
+ {
1243
+ name: "bq_invalid",
1244
+ type: "bigquery",
1245
+ bigqueryConnection: {
1246
+ defaultProjectId: "test-project",
1247
+ serviceAccountKeyJson: '{"invalid":"key"}',
1248
+ },
1132
1249
  },
1133
1250
  ],
1134
1251
  testProjectPath,
1135
1252
  ),
1136
- ).rejects.toThrow(/at least one attached database/);
1253
+ ).toThrow(/missing "type" field/);
1254
+ });
1255
+
1256
+ it("should preserve PGSSLMODE for project-level Postgres", () => {
1257
+ const previousPgSslMode = process.env.PGSSLMODE;
1258
+ process.env.PGSSLMODE = "require";
1259
+ try {
1260
+ const assembled = assembleProjectConnections(
1261
+ [
1262
+ {
1263
+ name: "pg_ssl",
1264
+ type: "postgres",
1265
+ postgresConnection: {
1266
+ host: "localhost",
1267
+ port: 5432,
1268
+ userName: "user",
1269
+ password: "pass",
1270
+ databaseName: "db",
1271
+ },
1272
+ },
1273
+ ],
1274
+ testProjectPath,
1275
+ );
1276
+
1277
+ expect(
1278
+ assembled.pojo.connections.pg_ssl.connectionString,
1279
+ ).toContain("sslmode=require");
1280
+ } finally {
1281
+ if (previousPgSslMode === undefined) {
1282
+ delete process.env.PGSSLMODE;
1283
+ } else {
1284
+ process.env.PGSSLMODE = previousPgSslMode;
1285
+ }
1286
+ }
1287
+ });
1288
+
1289
+ it("should use project-root-relative file paths for project-level DuckDB", async () => {
1290
+ const insideCsvPath = path.join(testProjectPath, "inside.csv");
1291
+ await fs.writeFile(insideCsvPath, "id\n1\n");
1292
+
1293
+ const { malloyConnections } = await createProjectConnections(
1294
+ [
1295
+ {
1296
+ name: "project_scoped_duckdb",
1297
+ type: "duckdb",
1298
+ duckdbConnection: { attachedDatabases: [] },
1299
+ },
1300
+ ],
1301
+ testProjectPath,
1302
+ );
1303
+
1304
+ const connection = malloyConnections.get(
1305
+ "project_scoped_duckdb",
1306
+ ) as DuckDBConnection;
1307
+ createdConnections.push(connection);
1308
+
1309
+ const assembled = assembleProjectConnections(
1310
+ [
1311
+ {
1312
+ name: "project_scoped_duckdb",
1313
+ type: "duckdb",
1314
+ duckdbConnection: { attachedDatabases: [] },
1315
+ },
1316
+ ],
1317
+ testProjectPath,
1318
+ );
1319
+ expect(
1320
+ assembled.pojo.connections.project_scoped_duckdb
1321
+ .workingDirectory,
1322
+ ).toBeUndefined();
1323
+ expect(
1324
+ assembled.pojo.connections.project_scoped_duckdb.securityPolicy,
1325
+ ).toBeUndefined();
1326
+
1327
+ await expect(
1328
+ connection.runSQL("SELECT * FROM read_csv_auto('inside.csv')"),
1329
+ ).resolves.toBeDefined();
1330
+ });
1331
+
1332
+ it("should keep external access available for federated DuckDB entries", () => {
1333
+ const assembled = assembleProjectConnections(
1334
+ [
1335
+ {
1336
+ name: "federated_duckdb",
1337
+ type: "duckdb",
1338
+ duckdbConnection: {
1339
+ attachedDatabases: [
1340
+ {
1341
+ name: "pg",
1342
+ type: "postgres",
1343
+ postgresConnection: {
1344
+ host: "localhost",
1345
+ port: 5432,
1346
+ userName: "user",
1347
+ password: "pass",
1348
+ databaseName: "db",
1349
+ },
1350
+ },
1351
+ ],
1352
+ },
1353
+ },
1354
+ ],
1355
+ testProjectPath,
1356
+ );
1357
+
1358
+ const entry = assembled.pojo.connections.federated_duckdb;
1359
+ expect(entry.securityPolicy).toBeUndefined();
1360
+ expect(entry.enableExternalAccess).toBeUndefined();
1361
+ expect(entry.allowedDirectories).toBeUndefined();
1362
+ });
1363
+
1364
+ it("should keep external access available for MotherDuck entries", () => {
1365
+ const assembled = assembleProjectConnections(
1366
+ [
1367
+ {
1368
+ name: "md",
1369
+ type: "motherduck",
1370
+ motherduckConnection: {
1371
+ accessToken: "token",
1372
+ database: "db",
1373
+ },
1374
+ },
1375
+ ],
1376
+ testProjectPath,
1377
+ );
1378
+
1379
+ const entry = assembled.pojo.connections.md;
1380
+ expect(entry.securityPolicy).toBeUndefined();
1381
+ expect(entry.enableExternalAccess).toBeUndefined();
1382
+ expect(entry.allowedDirectories).toBeUndefined();
1137
1383
  });
1138
1384
 
1139
1385
  it("should handle already attached database gracefully", async () => {