@malloy-publisher/server 0.0.75 → 0.0.76
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 +17 -1
- package/dist/instrumentation.js +1 -1
- package/dist/server.js +28005 -208
- package/package.json +4 -3
- package/src/dto/connection.dto.spec.ts +15 -0
- package/src/dto/connection.dto.ts +22 -0
- package/src/service/connection.ts +29 -1
- package/src/service/db_utils.ts +70 -20
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.76",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"malloy-publisher": "dist/server.js"
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@malloydata/db-bigquery": "^0.0.293",
|
|
25
25
|
"@malloydata/db-duckdb": "^0.0.293",
|
|
26
|
+
"@malloydata/db-mysql": "^0.0.293",
|
|
26
27
|
"@malloydata/db-postgres": "^0.0.293",
|
|
27
28
|
"@malloydata/db-snowflake": "^0.0.293",
|
|
28
29
|
"@malloydata/db-trino": "^0.0.293",
|
|
29
30
|
"@malloydata/malloy": "^0.0.293",
|
|
30
31
|
"@malloydata/malloy-sql": "^0.0.293",
|
|
31
32
|
"@malloydata/render": "^0.0.293",
|
|
32
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.13.2",
|
|
33
34
|
"@opentelemetry/api": "^1.9.0",
|
|
34
35
|
"@opentelemetry/auto-instrumentations-node": "^0.57.0",
|
|
35
36
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
@@ -41,11 +42,11 @@
|
|
|
41
42
|
"cors": "^2.8.5",
|
|
42
43
|
"express": "^4.21.0",
|
|
43
44
|
"globals": "^15.9.0",
|
|
45
|
+
"handlebars": "^4.7.8",
|
|
44
46
|
"http-proxy-middleware": "^3.0.5",
|
|
45
47
|
"morgan": "^1.10.0",
|
|
46
48
|
"node-cron": "^3.0.3",
|
|
47
49
|
"recursive-readdir": "^2.2.3",
|
|
48
|
-
"handlebars": "^4.7.8",
|
|
49
50
|
"uuid": "^11.0.3"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
@@ -7,10 +7,25 @@ import {
|
|
|
7
7
|
ConnectionDto,
|
|
8
8
|
PostgresConnectionDto,
|
|
9
9
|
SnowflakeConnectionDto,
|
|
10
|
+
MysqlConnectionDto,
|
|
10
11
|
} from "./connection.dto";
|
|
11
12
|
|
|
12
13
|
describe("dto/connection", () => {
|
|
13
14
|
describe("Connection Validation", () => {
|
|
15
|
+
it("should validate a valid MysqlConnection object", async () => {
|
|
16
|
+
const validData = {
|
|
17
|
+
host: "localhost",
|
|
18
|
+
port: 3306,
|
|
19
|
+
database: "testdb",
|
|
20
|
+
user: "user",
|
|
21
|
+
password: "pass",
|
|
22
|
+
};
|
|
23
|
+
const mysqlConnection = plainToInstance(MysqlConnectionDto, validData);
|
|
24
|
+
|
|
25
|
+
const errors = await validate(mysqlConnection);
|
|
26
|
+
expect(errors).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
14
29
|
it("should validate a valid PostgresConnection object", async () => {
|
|
15
30
|
const validData = {
|
|
16
31
|
host: "localhost",
|
|
@@ -35,6 +35,28 @@ export class PostgresConnectionDto {
|
|
|
35
35
|
connectionString?: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export class MysqlConnectionDto {
|
|
39
|
+
@IsOptional()
|
|
40
|
+
@IsString()
|
|
41
|
+
host?: string;
|
|
42
|
+
|
|
43
|
+
@IsOptional()
|
|
44
|
+
@IsNumber()
|
|
45
|
+
port?: number;
|
|
46
|
+
|
|
47
|
+
@IsOptional()
|
|
48
|
+
@IsString()
|
|
49
|
+
database?: string;
|
|
50
|
+
|
|
51
|
+
@IsOptional()
|
|
52
|
+
@IsString()
|
|
53
|
+
user?: string;
|
|
54
|
+
|
|
55
|
+
@IsOptional()
|
|
56
|
+
@IsString()
|
|
57
|
+
password?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
export class BigqueryConnectionDto {
|
|
39
61
|
@IsOptional()
|
|
40
62
|
@IsString()
|
|
@@ -2,6 +2,7 @@ import { PostgresConnection } from "@malloydata/db-postgres";
|
|
|
2
2
|
import { BigQueryConnection } from "@malloydata/db-bigquery";
|
|
3
3
|
import { SnowflakeConnection } from "@malloydata/db-snowflake";
|
|
4
4
|
import { TrinoConnection } from "@malloydata/db-trino";
|
|
5
|
+
import { MySQLConnection } from "@malloydata/db-mysql";
|
|
5
6
|
import { v4 as uuidv4 } from "uuid";
|
|
6
7
|
import { Connection } from "@malloydata/malloy";
|
|
7
8
|
import { components } from "../api";
|
|
@@ -20,6 +21,7 @@ export type InternalConnection = ApiConnection & {
|
|
|
20
21
|
bigqueryConnection?: components["schemas"]["BigqueryConnection"];
|
|
21
22
|
snowflakeConnection?: components["schemas"]["SnowflakeConnection"];
|
|
22
23
|
trinoConnection?: components["schemas"]["TrinoConnection"];
|
|
24
|
+
mysqlConnection?: components["schemas"]["MysqlConnection"];
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export async function readConnectionConfig(
|
|
@@ -82,6 +84,26 @@ export async function createConnections(basePath: string): Promise<{
|
|
|
82
84
|
break;
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
case "mysql": {
|
|
88
|
+
if (!connection.mysqlConnection) {
|
|
89
|
+
throw "Invalid connection configuration. No mysql connection.";
|
|
90
|
+
}
|
|
91
|
+
const config = {
|
|
92
|
+
host: connection.mysqlConnection.host,
|
|
93
|
+
port: connection.mysqlConnection.port,
|
|
94
|
+
username: connection.mysqlConnection.user,
|
|
95
|
+
password: connection.mysqlConnection.password,
|
|
96
|
+
database: connection.mysqlConnection.database,
|
|
97
|
+
};
|
|
98
|
+
const mysqlConnection = new MySQLConnection(
|
|
99
|
+
connection.name,
|
|
100
|
+
config,
|
|
101
|
+
);
|
|
102
|
+
connectionMap.set(connection.name, mysqlConnection);
|
|
103
|
+
connection.attributes = getConnectionAttributes(mysqlConnection);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
case "bigquery": {
|
|
86
108
|
if (!connection.bigqueryConnection) {
|
|
87
109
|
throw "Invalid connection configuration. No bigquery connection.";
|
|
@@ -209,10 +231,16 @@ export async function createConnections(basePath: string): Promise<{
|
|
|
209
231
|
function getConnectionAttributes(
|
|
210
232
|
connection: Connection,
|
|
211
233
|
): ApiConnectionAttributes {
|
|
234
|
+
let canStream = false;
|
|
235
|
+
try {
|
|
236
|
+
canStream = connection.canStream();
|
|
237
|
+
} catch {
|
|
238
|
+
// pass
|
|
239
|
+
}
|
|
212
240
|
return {
|
|
213
241
|
dialectName: connection.dialectName,
|
|
214
242
|
isPool: connection.isPool(),
|
|
215
243
|
canPersist: connection.canPersist(),
|
|
216
|
-
canStream:
|
|
244
|
+
canStream: canStream,
|
|
217
245
|
};
|
|
218
246
|
}
|
package/src/service/db_utils.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ApiConnection,
|
|
10
10
|
PostgresConnection,
|
|
11
11
|
SnowflakeConnection,
|
|
12
|
+
MysqlConnection,
|
|
12
13
|
} from "./model";
|
|
13
14
|
import { components } from "../api";
|
|
14
15
|
|
|
@@ -28,6 +29,21 @@ async function getPostgresConnection(
|
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
async function getMysqlConnection(apiMysqlConnection: MysqlConnection) {
|
|
33
|
+
// Dynamically import mysql2/promise to avoid import issues if not needed
|
|
34
|
+
const mysql = await import("mysql2/promise");
|
|
35
|
+
return mysql.createPool({
|
|
36
|
+
host: apiMysqlConnection.host,
|
|
37
|
+
port: apiMysqlConnection.port,
|
|
38
|
+
user: apiMysqlConnection.user,
|
|
39
|
+
password: apiMysqlConnection.password,
|
|
40
|
+
database: apiMysqlConnection.database,
|
|
41
|
+
waitForConnections: true,
|
|
42
|
+
connectionLimit: 10,
|
|
43
|
+
queueLimit: 0,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
function getBigqueryConnection(apiConnection: ApiConnection): BigQuery {
|
|
32
48
|
if (!apiConnection.bigqueryConnection?.serviceAccountKeyJson) {
|
|
33
49
|
// Use default credentials
|
|
@@ -74,22 +90,32 @@ export async function getSchemasForConnection(
|
|
|
74
90
|
if (!connection.bigqueryConnection) {
|
|
75
91
|
throw new Error("BigQuery connection is required");
|
|
76
92
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return datasets
|
|
85
|
-
.filter((dataset) => dataset.id)
|
|
86
|
-
.map((dataset) => {
|
|
87
|
-
return {
|
|
88
|
-
name: dataset.id,
|
|
89
|
-
isHidden: false,
|
|
90
|
-
isDefault: false,
|
|
91
|
-
};
|
|
93
|
+
try {
|
|
94
|
+
const bigquery = getBigqueryConnection(connection);
|
|
95
|
+
// Set the projectId if it's provided in the bigqueryConnection
|
|
96
|
+
const [datasets] = await bigquery.getDatasets({
|
|
97
|
+
...(connection.bigqueryConnection.defaultProjectId
|
|
98
|
+
? { projectId: connection.bigqueryConnection.defaultProjectId }
|
|
99
|
+
: {}),
|
|
92
100
|
});
|
|
101
|
+
return datasets
|
|
102
|
+
.filter((dataset) => dataset.id)
|
|
103
|
+
.map((dataset) => {
|
|
104
|
+
return {
|
|
105
|
+
name: dataset.id,
|
|
106
|
+
isHidden: false,
|
|
107
|
+
isDefault: false,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(
|
|
112
|
+
`Error getting schemas for BigQuery connection ${connection.name}:`,
|
|
113
|
+
error,
|
|
114
|
+
);
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Failed to get schemas for BigQuery connection ${connection.name}: ${(error as Error).message}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
93
119
|
} else if (connection.type === "postgres") {
|
|
94
120
|
if (!connection.postgresConnection) {
|
|
95
121
|
throw new Error("Postgres connection is required");
|
|
@@ -107,6 +133,17 @@ export async function getSchemasForConnection(
|
|
|
107
133
|
isDefault: row.schema_name === "public",
|
|
108
134
|
};
|
|
109
135
|
});
|
|
136
|
+
} else if (connection.type === "mysql") {
|
|
137
|
+
if (!connection.mysqlConnection) {
|
|
138
|
+
throw new Error("Mysql connection is required");
|
|
139
|
+
}
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
name: connection.mysqlConnection.database || "mysql",
|
|
143
|
+
isHidden: false,
|
|
144
|
+
isDefault: true,
|
|
145
|
+
},
|
|
146
|
+
];
|
|
110
147
|
} else if (connection.type === "snowflake") {
|
|
111
148
|
if (!connection.snowflakeConnection) {
|
|
112
149
|
throw new Error("Snowflake connection is required");
|
|
@@ -133,8 +170,8 @@ export async function getTablesForSchema(
|
|
|
133
170
|
schemaName: string,
|
|
134
171
|
): Promise<string[]> {
|
|
135
172
|
if (connection.type === "bigquery") {
|
|
136
|
-
const bigquery = getBigqueryConnection(connection);
|
|
137
173
|
try {
|
|
174
|
+
const bigquery = getBigqueryConnection(connection);
|
|
138
175
|
const options = connection.bigqueryConnection?.defaultProjectId
|
|
139
176
|
? {
|
|
140
177
|
projectId: connection.bigqueryConnection?.defaultProjectId,
|
|
@@ -151,11 +188,24 @@ export async function getTablesForSchema(
|
|
|
151
188
|
const [tables] = await dataset.getTables();
|
|
152
189
|
return tables.map((table) => table.id).filter((id) => id) as string[];
|
|
153
190
|
} catch (error) {
|
|
154
|
-
console.error(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
191
|
+
console.error(
|
|
192
|
+
`Error getting tables for BigQuery schema ${schemaName} in connection ${connection.name}:`,
|
|
193
|
+
error,
|
|
194
|
+
);
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Failed to get tables for BigQuery schema ${schemaName} in connection ${connection.name}: ${(error as Error).message}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
} else if (connection.type === "mysql") {
|
|
200
|
+
if (!connection.mysqlConnection) {
|
|
201
|
+
throw new Error("Mysql connection is required");
|
|
158
202
|
}
|
|
203
|
+
const pool = await getMysqlConnection(connection.mysqlConnection);
|
|
204
|
+
const [rows] = await pool.query(
|
|
205
|
+
"SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = ? AND table_type = 'BASE TABLE'",
|
|
206
|
+
[schemaName],
|
|
207
|
+
);
|
|
208
|
+
return (rows as { TABLE_NAME: string }[]).map((row) => row.TABLE_NAME);
|
|
159
209
|
} else if (connection.type === "postgres") {
|
|
160
210
|
if (!connection.postgresConnection) {
|
|
161
211
|
throw new Error("Postgres connection is required");
|