@malloy-publisher/server 0.0.192 → 0.0.194

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 (44) hide show
  1. package/build.ts +1 -0
  2. package/dist/app/api-doc.yaml +558 -1
  3. package/dist/app/assets/{HomePage-H1OH-VW5.js → HomePage-DbZS0N7G.js} +1 -1
  4. package/dist/app/assets/MainPage-CBuWkbmr.js +2 -0
  5. package/dist/app/assets/{ModelPage-Crau5hgZ.js → ModelPage-Bt37smot.js} +1 -1
  6. package/dist/app/assets/{PackagePage-CbubRhgE.js → PackagePage-DLZe50WG.js} +1 -1
  7. package/dist/app/assets/{ProjectPage-DUlJkYJ4.js → ProjectPage-FQTEPXP4.js} +1 -1
  8. package/dist/app/assets/{RouteError-DrNXNihc.js → RouteError-DefbDO7F.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-CBBv7n5U.js → WorkbookPage-CkAo16ar.js} +1 -1
  10. package/dist/app/assets/{core-Dzx75uJR.es-DwnFZnyO.js → core-BrfQApxh.es-DnvCX4oH.js} +14 -14
  11. package/dist/app/assets/index-5eLCcNmP.css +1 -0
  12. package/dist/app/assets/{index-d5rvmoZ7.js → index-Bu0ub036.js} +119 -119
  13. package/dist/app/assets/index-CkzK3JIl.js +40 -0
  14. package/dist/app/assets/index-CoA6HIGS.js +1742 -0
  15. package/dist/app/assets/{index.umd-CetYIBQY.js → index.umd-B6Ms2PpL.js} +46 -46
  16. package/dist/app/index.html +2 -2
  17. package/dist/server.mjs +1529 -985
  18. package/package.json +11 -10
  19. package/src/config.ts +7 -2
  20. package/src/controller/connection.controller.ts +102 -27
  21. package/src/dto/connection.dto.spec.ts +55 -0
  22. package/src/dto/connection.dto.ts +87 -2
  23. package/src/server.ts +201 -2
  24. package/src/service/connection.spec.ts +250 -4
  25. package/src/service/connection.ts +328 -473
  26. package/src/service/connection_config.spec.ts +123 -0
  27. package/src/service/connection_config.ts +562 -0
  28. package/src/service/connection_service.spec.ts +50 -0
  29. package/src/service/connection_service.ts +125 -32
  30. package/src/service/db_utils.spec.ts +161 -0
  31. package/src/service/db_utils.ts +131 -0
  32. package/src/service/materialization_service.spec.ts +18 -12
  33. package/src/service/materialization_service.ts +54 -7
  34. package/src/service/model.ts +24 -27
  35. package/src/service/package.spec.ts +125 -1
  36. package/src/service/package.ts +86 -44
  37. package/src/service/project.ts +172 -94
  38. package/src/service/project_store.spec.ts +72 -0
  39. package/src/service/project_store.ts +98 -81
  40. package/tests/unit/duckdb/attached_databases.test.ts +1 -19
  41. package/dist/app/assets/MainPage-GL06aMke.js +0 -2
  42. package/dist/app/assets/index-CMlGQMcl.css +0 -1
  43. package/dist/app/assets/index-CzjyS9cx.js +0 -1276
  44. package/dist/app/assets/index-HHdhLUpv.js +0 -676
package/src/server.ts CHANGED
@@ -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,6 +693,30 @@ 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) => {
@@ -617,8 +746,35 @@ app.post(
617
746
  },
618
747
  );
619
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,
766
+ ),
767
+ );
768
+ } catch (error) {
769
+ logger.error(error);
770
+ const { json, status } = internalErrorToHttpError(error as Error);
771
+ res.status(status).json(json);
772
+ }
773
+ },
774
+ );
775
+
620
776
  /**
621
- * @deprecated Use /projects/:projectName/connections/:connectionName/temporaryTable POST method instead
777
+ * @deprecated Use /projects/:projectName/connections/:connectionName/sqlTemporaryTable POST method instead
622
778
  */
623
779
  app.get(
624
780
  `${API_PREFIX}/projects/:projectName/connections/:connectionName/temporaryTable`,
@@ -639,6 +795,29 @@ app.get(
639
795
  },
640
796
  );
641
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
+
642
821
  app.post(
643
822
  `${API_PREFIX}/projects/:projectName/connections/:connectionName/sqlTemporaryTable`,
644
823
  async (req, res) => {
@@ -658,6 +837,26 @@ app.post(
658
837
  },
659
838
  );
660
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
+
661
860
  app.get(`${API_PREFIX}/projects/:projectName/packages`, async (req, res) => {
662
861
  if (req.query.versionId) {
663
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 () => {