@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/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.191",
4
+ "version": "0.0.193",
5
5
  "main": "dist/server.mjs",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.mjs"
@@ -34,15 +34,15 @@
34
34
  "@azure/identity": "^4.13.0",
35
35
  "@azure/storage-blob": "^12.26.0",
36
36
  "@google-cloud/storage": "^7.16.0",
37
- "@malloydata/db-bigquery": "^0.0.377",
38
- "@malloydata/db-duckdb": "^0.0.377",
39
- "@malloydata/db-mysql": "^0.0.377",
40
- "@malloydata/db-postgres": "^0.0.377",
41
- "@malloydata/db-snowflake": "^0.0.377",
42
- "@malloydata/db-trino": "^0.0.377",
43
- "@malloydata/malloy": "^0.0.377",
44
- "@malloydata/malloy-sql": "^0.0.377",
45
- "@malloydata/render-validator": "^0.0.377",
37
+ "@malloydata/db-bigquery": "^0.0.383",
38
+ "@malloydata/db-duckdb": "^0.0.383",
39
+ "@malloydata/db-mysql": "^0.0.383",
40
+ "@malloydata/db-postgres": "^0.0.383",
41
+ "@malloydata/db-snowflake": "^0.0.383",
42
+ "@malloydata/db-trino": "^0.0.383",
43
+ "@malloydata/malloy": "^0.0.383",
44
+ "@malloydata/malloy-sql": "^0.0.383",
45
+ "@malloydata/render-validator": "^0.0.383",
46
46
  "@modelcontextprotocol/sdk": "^1.13.2",
47
47
  "@opentelemetry/api": "^1.9.0",
48
48
  "@opentelemetry/auto-instrumentations-node": "^0.57.0",
@@ -4,11 +4,13 @@ import { components } from "../api";
4
4
  import { BadRequestError, ConnectionError } from "../errors";
5
5
  import { logger } from "../logger";
6
6
  import { testConnectionConfig } from "../service/connection";
7
+ import { validateDuckdbApiSurface } from "../service/connection_config";
7
8
  import { ConnectionService } from "../service/connection_service";
8
9
  import {
9
10
  getSchemasForConnection,
10
11
  listTablesForSchema,
11
12
  } from "../service/db_utils";
13
+ import type { Project } from "../service/project";
12
14
  import { ProjectStore } from "../service/project_store";
13
15
 
14
16
  type ApiConnection = components["schemas"]["Connection"];
@@ -83,6 +85,23 @@ function validateAzureAttachedDatabases(connectionConfig: ApiConnection): void {
83
85
  }
84
86
  }
85
87
 
88
+ function validateAdminAuthoredConnection(
89
+ connectionName: string,
90
+ connectionConfig: ApiConnection,
91
+ ): void {
92
+ if (connectionName === "duckdb" || connectionConfig.name === "duckdb") {
93
+ throw new BadRequestError(
94
+ "DuckDB connection name cannot be 'duckdb'; it is reserved for Publisher package sandboxes.",
95
+ );
96
+ }
97
+
98
+ try {
99
+ validateDuckdbApiSurface(connectionConfig);
100
+ } catch (error) {
101
+ throw new BadRequestError((error as Error).message);
102
+ }
103
+ }
104
+
86
105
  export class ConnectionController {
87
106
  private projectStore: ProjectStore;
88
107
  private connectionService: ConnectionService;
@@ -95,28 +114,62 @@ export class ConnectionController {
95
114
  * Gets the appropriate Malloy connection for a given connection name.
96
115
  * For DuckDB connections, retrieves from package level; for others, from project level.
97
116
  */
117
+ private getApiConnectionForLookup(
118
+ project: Project,
119
+ connectionName: string,
120
+ ): ApiConnection {
121
+ if (connectionName === "duckdb") {
122
+ return {
123
+ name: "duckdb",
124
+ type: "duckdb",
125
+ duckdbConnection: { attachedDatabases: [] },
126
+ };
127
+ }
128
+ return project.getApiConnection(connectionName);
129
+ }
130
+
98
131
  private async getMalloyConnection(
99
132
  projectName: string,
100
133
  connectionName: string,
134
+ packageName?: string,
101
135
  ): Promise<Connection> {
102
136
  const project = await this.projectStore.getProject(projectName, false);
103
- const connection = project.getApiConnection(connectionName);
104
137
 
105
- // For DuckDB connections, get the connection from a package
106
- if (connection.name === "duckdb" && connection.type === "duckdb") {
138
+ // "duckdb" is the per-package sandbox; its rootDirectory is the
139
+ // package's directory. There is no project-level "duckdb" the name is
140
+ // reserved at config time. So the lookup is intrinsically per-package
141
+ // and the caller must say which package to use.
142
+ if (connectionName === "duckdb") {
107
143
  const packages = await project.listPackages();
108
144
  if (packages.length === 0) {
109
- return project.getMalloyConnection(connectionName);
145
+ // Fall through to project; this will surface the standard
146
+ // "connection not found" rather than silently inventing one.
147
+ return await project.getMalloyConnection(connectionName);
148
+ }
149
+ if (packageName) {
150
+ const known = packages.some((p) => p.name === packageName);
151
+ if (!known) {
152
+ throw new BadRequestError(
153
+ `Package "${packageName}" not found in project "${projectName}"`,
154
+ );
155
+ }
156
+ const pkg = await project.getPackage(packageName);
157
+ return await pkg.getMalloyConnection(connectionName);
110
158
  }
111
- // For now, use the first package's DuckDB connection
112
- const packageName = packages[0].name;
113
- if (!packageName) {
114
- throw new ConnectionError("Package name is undefined");
159
+ if (packages.length === 1) {
160
+ const onlyPackage = packages[0].name;
161
+ if (!onlyPackage) {
162
+ throw new ConnectionError("Package name is undefined");
163
+ }
164
+ const pkg = await project.getPackage(onlyPackage);
165
+ return await pkg.getMalloyConnection(connectionName);
115
166
  }
116
- const pkg = await project.getPackage(packageName);
117
- return pkg.getMalloyConnection(connectionName);
167
+ throw new BadRequestError(
168
+ `Ambiguous "duckdb" connection lookup: project "${projectName}" has multiple packages. ` +
169
+ `Use /projects/${projectName}/packages/{packageName}/connections/duckdb/... to disambiguate.`,
170
+ );
118
171
  } else {
119
- return project.getMalloyConnection(connectionName);
172
+ return await project.getMalloyConnection(connectionName);
120
173
  }
121
174
  }
122
175
 
@@ -188,33 +241,46 @@ export class ConnectionController {
188
241
  return project.listApiConnections();
189
242
  }
190
243
 
191
- // Lists schemas (namespaces) available in a connection
244
+ // Lists schemas (namespaces) available in a connection.
245
+ // For "duckdb", the per-package sandbox, packageName disambiguates which
246
+ // package's DuckDB to browse in a multi-package project.
192
247
  public async listSchemas(
193
248
  projectName: string,
194
249
  connectionName: string,
250
+ packageName?: string,
195
251
  ): Promise<ApiSchema[]> {
196
252
  const project = await this.projectStore.getProject(projectName, false);
197
- const connection = project.getApiConnection(connectionName);
253
+ const connection = this.getApiConnectionForLookup(
254
+ project,
255
+ connectionName,
256
+ );
198
257
  const malloyConnection = await this.getMalloyConnection(
199
258
  projectName,
200
259
  connectionName,
260
+ packageName,
201
261
  );
202
262
 
203
263
  return getSchemasForConnection(connection, malloyConnection);
204
264
  }
205
265
 
206
- // Lists tables available in a schema. For postgres the schema is usually "public"
266
+ // Lists tables available in a schema. For postgres the schema is usually "public".
267
+ // packageName disambiguates per-package "duckdb" lookups (see listSchemas).
207
268
  public async listTables(
208
269
  projectName: string,
209
270
  connectionName: string,
210
271
  schemaName: string,
211
272
  tableNames?: string[],
273
+ packageName?: string,
212
274
  ): Promise<ApiTable[]> {
213
275
  const project = await this.projectStore.getProject(projectName, false);
214
- const connection = project.getApiConnection(connectionName);
276
+ const connection = this.getApiConnectionForLookup(
277
+ project,
278
+ connectionName,
279
+ );
215
280
  const malloyConnection = await this.getMalloyConnection(
216
281
  projectName,
217
282
  connectionName,
283
+ packageName,
218
284
  );
219
285
 
220
286
  return listTablesForSchema(
@@ -229,10 +295,12 @@ export class ConnectionController {
229
295
  projectName: string,
230
296
  connectionName: string,
231
297
  sqlStatement: string,
298
+ packageName?: string,
232
299
  ): Promise<ApiSqlSource> {
233
300
  const malloyConnection = await this.getMalloyConnection(
234
301
  projectName,
235
302
  connectionName,
303
+ packageName,
236
304
  );
237
305
  try {
238
306
  const schema = await (
@@ -265,14 +333,19 @@ export class ConnectionController {
265
333
  connectionName: string,
266
334
  schemaName: string,
267
335
  tablePath: string,
336
+ packageName?: string,
268
337
  ): Promise<ApiTable> {
269
338
  const malloyConnection = await this.getMalloyConnection(
270
339
  projectName,
271
340
  connectionName,
341
+ packageName,
272
342
  );
273
343
  // Use getApiConnection to get the unwrapped ApiConnection config, consistent with listSchemas and listTables.
274
344
  const project = await this.projectStore.getProject(projectName, false);
275
- const connection = project.getApiConnection(connectionName);
345
+ const connection = this.getApiConnectionForLookup(
346
+ project,
347
+ connectionName,
348
+ );
276
349
 
277
350
  // TODO: Move this database connection logic to the db_utils.ts file -- and
278
351
  // ultimately into a connection-specific class.
@@ -346,10 +419,12 @@ export class ConnectionController {
346
419
  connectionName: string,
347
420
  sqlStatement: string,
348
421
  options: string,
422
+ packageName?: string,
349
423
  ): Promise<ApiQueryData> {
350
424
  const malloyConnection = await this.getMalloyConnection(
351
425
  projectName,
352
426
  connectionName,
427
+ packageName,
353
428
  );
354
429
 
355
430
  let runSQLOptions: RunSQLOptions = {};
@@ -377,10 +452,12 @@ export class ConnectionController {
377
452
  projectName: string,
378
453
  connectionName: string,
379
454
  sqlStatement: string,
455
+ packageName?: string,
380
456
  ): Promise<ApiTemporaryTable> {
381
457
  const malloyConnection = await this.getMalloyConnection(
382
458
  projectName,
383
459
  connectionName,
460
+ packageName,
384
461
  );
385
462
 
386
463
  try {
@@ -397,17 +474,13 @@ export class ConnectionController {
397
474
  }
398
475
 
399
476
  public async testConnectionConfiguration(
400
- connectionConfig: ApiConnection,
477
+ input: ApiConnection & { config?: ApiConnection },
401
478
  ): Promise<ApiConnectionStatus> {
402
- if (
403
- connectionConfig &&
404
- "config" in connectionConfig &&
405
- typeof (connectionConfig as Record<string, unknown>).config ===
406
- "object"
407
- ) {
408
- connectionConfig = (connectionConfig as Record<string, unknown>)
409
- .config as ApiConnection;
410
- }
479
+ // Some clients wrap the payload as { config: <connection> }; unwrap.
480
+ const connectionConfig: ApiConnection =
481
+ input.config && typeof input.config === "object"
482
+ ? input.config
483
+ : input;
411
484
 
412
485
  if (
413
486
  !connectionConfig ||
@@ -453,6 +526,7 @@ export class ConnectionController {
453
526
  }
454
527
 
455
528
  validateAzureAttachedDatabases(connectionConfig);
529
+ validateAdminAuthoredConnection(connectionName, connectionConfig);
456
530
 
457
531
  logger.info(
458
532
  `Creating connection "${connectionName}" in project "${projectName}"`,
@@ -478,7 +552,8 @@ export class ConnectionController {
478
552
  throw new BadRequestError("Connection payload is required");
479
553
  }
480
554
 
481
- validateAzureAttachedDatabases(connection as ApiConnection);
555
+ validateAzureAttachedDatabases(connection);
556
+ validateAdminAuthoredConnection(connectionName, connection);
482
557
 
483
558
  logger.info(
484
559
  `Updating connection "${connectionName}" in project "${projectName}"`,
@@ -82,9 +82,13 @@ describe("dto/connection", () => {
82
82
  account: "my-account",
83
83
  username: "user",
84
84
  password: "pass",
85
+ privateKey:
86
+ "-----BEGIN PRIVATE KEY-----\\nabc\\n-----END PRIVATE KEY-----",
87
+ privateKeyPass: "secret",
85
88
  warehouse: "my-warehouse",
86
89
  database: "my-database",
87
90
  schema: "my-schema",
91
+ role: "analyst",
88
92
  responseTimeoutMilliseconds: 5000,
89
93
  };
90
94
  const snowflakeConnection = plainToInstance(
@@ -1,14 +1,18 @@
1
1
  import { Type } from "class-transformer";
2
2
  import {
3
3
  IsEnum,
4
+ IsArray,
4
5
  IsNumber,
5
6
  IsOptional,
6
7
  IsString,
7
8
  ValidateNested,
8
9
  } from "class-validator";
9
10
  import "reflect-metadata";
11
+ import { components } from "../api";
10
12
  import { ApiConnection } from "../service/model";
11
13
 
14
+ type AttachedDatabase = components["schemas"]["AttachedDatabase"];
15
+
12
16
  export class PostgresConnectionDto {
13
17
  @IsOptional()
14
18
  @IsString()
@@ -96,6 +100,14 @@ export class SnowflakeConnectionDto {
96
100
  @IsString()
97
101
  password?: string;
98
102
 
103
+ @IsOptional()
104
+ @IsString()
105
+ privateKey?: string;
106
+
107
+ @IsOptional()
108
+ @IsString()
109
+ privateKeyPass?: string;
110
+
99
111
  @IsOptional()
100
112
  @IsString()
101
113
  warehouse?: string;
@@ -108,6 +120,10 @@ export class SnowflakeConnectionDto {
108
120
  @IsString()
109
121
  schema?: string;
110
122
 
123
+ @IsOptional()
124
+ @IsString()
125
+ role?: string;
126
+
111
127
  @IsOptional()
112
128
  @IsNumber()
113
129
  responseTimeoutMilliseconds?: number;
@@ -143,14 +159,37 @@ export class TrinoConnectionDto {
143
159
  peakaKey?: string;
144
160
  }
145
161
 
162
+ export class DuckdbConnectionDto {
163
+ @IsOptional()
164
+ @IsArray()
165
+ attachedDatabases?: AttachedDatabase[];
166
+ }
167
+
146
168
  export class ConnectionDto implements ApiConnection {
147
169
  @IsOptional()
148
170
  @IsString()
149
171
  name?: string;
150
172
 
151
173
  @IsOptional()
152
- @IsEnum(["postgres", "bigquery", "snowflake", "trino"])
153
- type?: "postgres" | "bigquery" | "snowflake" | "trino";
174
+ @IsEnum([
175
+ "postgres",
176
+ "bigquery",
177
+ "snowflake",
178
+ "trino",
179
+ "mysql",
180
+ "duckdb",
181
+ "motherduck",
182
+ "ducklake",
183
+ ])
184
+ type?:
185
+ | "postgres"
186
+ | "bigquery"
187
+ | "snowflake"
188
+ | "trino"
189
+ | "mysql"
190
+ | "duckdb"
191
+ | "motherduck"
192
+ | "ducklake";
154
193
 
155
194
  @IsOptional()
156
195
  @ValidateNested()
@@ -171,4 +210,9 @@ export class ConnectionDto implements ApiConnection {
171
210
  @ValidateNested()
172
211
  @Type(() => TrinoConnectionDto)
173
212
  TrinoConnection?: TrinoConnectionDto;
213
+
214
+ @IsOptional()
215
+ @ValidateNested()
216
+ @Type(() => DuckdbConnectionDto)
217
+ duckdbConnection?: DuckdbConnectionDto;
174
218
  }