@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.
- package/dist/app/api-doc.yaml +531 -3
- package/dist/app/assets/{HomePage-Dn3E4CuB.js → HomePage-Di9MU3lS.js} +1 -1
- package/dist/app/assets/{MainPage-BzB3yoqi.js → MainPage-yZQo2HSL.js} +1 -1
- package/dist/app/assets/{ModelPage-C9O_sAXT.js → ModelPage-Dx2mHWeT.js} +1 -1
- package/dist/app/assets/{PackagePage-DcxKEjBX.js → PackagePage-Q386Py9t.js} +1 -1
- package/dist/app/assets/{ProjectPage-BDj307rF.js → ProjectPage-WR7wPQB-.js} +1 -1
- package/dist/app/assets/{RouteError-DAShbVCG.js → RouteError-stRGU4aW.js} +1 -1
- package/dist/app/assets/{WorkbookPage-Cs_XYEaB.js → WorkbookPage-D3iX0djH.js} +1 -1
- package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-QH4HZQVz.es-CqlQLZdl.js} +1 -1
- package/dist/app/assets/{index-15BOvhp0.js → index-CVHzPJwN.js} +119 -119
- package/dist/app/assets/{index-D68X76-7.js → index-DavAceYD.js} +50 -50
- package/dist/app/assets/{index-Bb2jqquW.js → index-Y3Y-VRna.js} +1 -1
- package/dist/app/assets/{index.umd-DGBekgSu.js → index.umd-Bp8OIhfV.js} +46 -46
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +1396 -985
- package/package.json +10 -10
- package/src/controller/connection.controller.ts +102 -27
- package/src/dto/connection.dto.spec.ts +4 -0
- package/src/dto/connection.dto.ts +46 -2
- package/src/server.ts +217 -9
- package/src/service/connection.spec.ts +250 -4
- package/src/service/connection.ts +326 -473
- package/src/service/connection_config.ts +514 -0
- package/src/service/connection_service.spec.ts +50 -0
- package/src/service/connection_service.ts +125 -32
- package/src/service/materialization_service.spec.ts +18 -12
- package/src/service/materialization_service.ts +54 -7
- package/src/service/model.ts +24 -27
- package/src/service/package.spec.ts +125 -1
- package/src/service/package.ts +86 -44
- package/src/service/project.ts +172 -94
- package/src/service/project_store.spec.ts +72 -0
- package/src/service/project_store.ts +98 -81
- 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.
|
|
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.
|
|
38
|
-
"@malloydata/db-duckdb": "^0.0.
|
|
39
|
-
"@malloydata/db-mysql": "^0.0.
|
|
40
|
-
"@malloydata/db-postgres": "^0.0.
|
|
41
|
-
"@malloydata/db-snowflake": "^0.0.
|
|
42
|
-
"@malloydata/db-trino": "^0.0.
|
|
43
|
-
"@malloydata/malloy": "^0.0.
|
|
44
|
-
"@malloydata/malloy-sql": "^0.0.
|
|
45
|
-
"@malloydata/render-validator": "^0.0.
|
|
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
|
-
//
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
477
|
+
input: ApiConnection & { config?: ApiConnection },
|
|
401
478
|
): Promise<ApiConnectionStatus> {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
|
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([
|
|
153
|
-
|
|
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
|
}
|