@malloy-publisher/server 0.0.192 → 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 +522 -1
  2. package/dist/app/assets/{HomePage-H1OH-VW5.js → HomePage-Di9MU3lS.js} +1 -1
  3. package/dist/app/assets/{MainPage-GL06aMke.js → MainPage-yZQo2HSL.js} +1 -1
  4. package/dist/app/assets/{ModelPage-Crau5hgZ.js → ModelPage-Dx2mHWeT.js} +1 -1
  5. package/dist/app/assets/{PackagePage-CbubRhgE.js → PackagePage-Q386Py9t.js} +1 -1
  6. package/dist/app/assets/{ProjectPage-DUlJkYJ4.js → ProjectPage-WR7wPQB-.js} +1 -1
  7. package/dist/app/assets/{RouteError-DrNXNihc.js → RouteError-stRGU4aW.js} +1 -1
  8. package/dist/app/assets/{WorkbookPage-CBBv7n5U.js → WorkbookPage-D3iX0djH.js} +1 -1
  9. package/dist/app/assets/{core-Dzx75uJR.es-DwnFZnyO.js → core-QH4HZQVz.es-CqlQLZdl.js} +1 -1
  10. package/dist/app/assets/{index-d5rvmoZ7.js → index-CVHzPJwN.js} +119 -119
  11. package/dist/app/assets/{index-CzjyS9cx.js → index-DavAceYD.js} +50 -50
  12. package/dist/app/assets/{index-HHdhLUpv.js → index-Y3Y-VRna.js} +1 -1
  13. package/dist/app/assets/{index.umd-CetYIBQY.js → index.umd-Bp8OIhfV.js} +46 -46
  14. package/dist/app/index.html +1 -1
  15. package/dist/server.mjs +1389 -984
  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 +201 -2
  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
@@ -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 () => {