@malloy-publisher/server 0.0.87 → 0.0.89

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 (35) hide show
  1. package/build.ts +26 -0
  2. package/dist/app/api-doc.yaml +126 -5
  3. package/dist/app/assets/RenderedResult-BAZuT25g-QakVAbYy.js +2 -0
  4. package/dist/app/assets/{index-BbW5TZg_.js → index-Bq29VQqL.js} +2 -2
  5. package/dist/app/assets/{index-2xWCh-ya.css → index-CcIq0aEZ.css} +1 -1
  6. package/dist/app/assets/index-DZMePHJ5.js +251 -0
  7. package/dist/app/assets/{index-CIfV3yj1.js → index-TslDWlxH.js} +6 -6
  8. package/dist/app/assets/{index.umd-x-naS8R7.js → index.umd-BN4_E5KD.js} +259 -259
  9. package/dist/app/assets/mui-BEbinrI-.js +161 -0
  10. package/dist/app/assets/vendor-c5ypKtDW.js +17 -0
  11. package/dist/app/index.html +4 -2
  12. package/dist/instrumentation.js +67818 -35196
  13. package/dist/server.js +80404 -82231
  14. package/package.json +11 -5
  15. package/publisher.config.json +1 -1
  16. package/src/config.ts +20 -0
  17. package/src/constants.ts +14 -0
  18. package/src/controller/connection.controller.ts +21 -4
  19. package/src/controller/package.controller.ts +52 -2
  20. package/src/controller/schedule.controller.ts +3 -3
  21. package/src/controller/watch-mode.controller.ts +83 -0
  22. package/src/errors.ts +2 -1
  23. package/src/logger.ts +9 -0
  24. package/src/server.ts +33 -19
  25. package/src/service/connection.ts +159 -161
  26. package/src/service/model.ts +6 -6
  27. package/src/service/package.spec.ts +12 -10
  28. package/src/service/package.ts +15 -8
  29. package/src/service/project.ts +77 -36
  30. package/src/service/project_store.spec.ts +83 -56
  31. package/src/service/project_store.ts +330 -50
  32. package/src/utils.ts +0 -18
  33. package/tests/harness/mcp_test_setup.ts +5 -5
  34. package/dist/app/assets/RenderedResult-BAZuT25g-BMU632YI.js +0 -2
  35. package/dist/app/assets/index-C7whj6wK.js +0 -432
@@ -1,15 +1,16 @@
1
- import { PostgresConnection } from "@malloydata/db-postgres";
2
1
  import { BigQueryConnection } from "@malloydata/db-bigquery";
2
+ import { MySQLConnection } from "@malloydata/db-mysql";
3
+ import { PostgresConnection } from "@malloydata/db-postgres";
3
4
  import { SnowflakeConnection } from "@malloydata/db-snowflake";
4
5
  import { TrinoConnection } from "@malloydata/db-trino";
5
- import { MySQLConnection } from "@malloydata/db-mysql";
6
- import { v4 as uuidv4 } from "uuid";
7
6
  import { Connection } from "@malloydata/malloy";
8
- import { components } from "../api";
9
- import path from "path";
10
- import fs from "fs/promises";
11
- import { CONNECTIONS_MANIFEST_NAME } from "../utils";
12
7
  import { BaseConnection } from "@malloydata/malloy/connection";
8
+ import fs from "fs/promises";
9
+ import path from "path";
10
+ import { v4 as uuidv4 } from "uuid";
11
+ import { components } from "../api";
12
+ import { CONNECTIONS_MANIFEST_NAME } from "../constants";
13
+ import { logger } from "../logger";
13
14
 
14
15
  type ApiConnection = components["schemas"]["Connection"];
15
16
  type ApiConnectionAttributes = components["schemas"]["ConnectionAttributes"];
@@ -42,184 +43,181 @@ export async function readConnectionConfig(
42
43
  return JSON.parse(connectionFileContents.toString()) as ApiConnection[];
43
44
  }
44
45
 
45
- export async function createConnections(basePath: string): Promise<{
46
+ export async function createConnections(
47
+ basePath: string,
48
+ defaultConnections: ApiConnection[] = [],
49
+ ): Promise<{
46
50
  malloyConnections: Map<string, BaseConnection>;
47
51
  apiConnections: InternalConnection[];
48
52
  }> {
49
53
  const connectionMap = new Map<string, BaseConnection>();
50
54
  const connectionConfig = await readConnectionConfig(basePath);
51
55
 
52
- if (connectionConfig.length > 0) {
53
- connectionConfig.map(async (connection) => {
54
- // This case shouldn't happen. The package validation logic should
55
- // catch it.
56
- if (!connection.name) {
57
- throw "Invalid connection configuration. No name.";
56
+ for (const connection of [...defaultConnections, ...connectionConfig]) {
57
+ logger.info(`Adding connection ${connection.name}`, {
58
+ connection,
59
+ });
60
+ // This case shouldn't happen. The package validation logic should
61
+ // catch it.
62
+ if (!connection.name) {
63
+ throw "Invalid connection configuration. No name.";
64
+ }
65
+
66
+ switch (connection.type) {
67
+ case "postgres": {
68
+ const configReader = async () => {
69
+ if (!connection.postgresConnection) {
70
+ throw "Invalid connection configuration. No postgres connection.";
71
+ }
72
+ return {
73
+ host: connection.postgresConnection.host,
74
+ port: connection.postgresConnection.port,
75
+ username: connection.postgresConnection.userName,
76
+ password: connection.postgresConnection.password,
77
+ databaseName: connection.postgresConnection.databaseName,
78
+ connectionString:
79
+ connection.postgresConnection.connectionString,
80
+ };
81
+ };
82
+ const postgresConnection = new PostgresConnection(
83
+ connection.name,
84
+ () => ({}),
85
+ configReader,
86
+ );
87
+ connectionMap.set(connection.name, postgresConnection);
88
+ connection.attributes = getConnectionAttributes(postgresConnection);
89
+ break;
58
90
  }
59
91
 
60
- switch (connection.type) {
61
- case "postgres": {
62
- const configReader = async () => {
63
- if (!connection.postgresConnection) {
64
- throw "Invalid connection configuration. No postgres connection.";
65
- }
66
- return {
67
- host: connection.postgresConnection.host,
68
- port: connection.postgresConnection.port,
69
- username: connection.postgresConnection.userName,
70
- password: connection.postgresConnection.password,
71
- databaseName: connection.postgresConnection.databaseName,
72
- connectionString:
73
- connection.postgresConnection.connectionString,
74
- };
75
- };
76
- const postgresConnection = new PostgresConnection(
77
- connection.name,
78
- () => ({}),
79
- configReader,
80
- );
81
- connectionMap.set(connection.name, postgresConnection);
82
- connection.attributes =
83
- getConnectionAttributes(postgresConnection);
84
- break;
92
+ case "mysql": {
93
+ if (!connection.mysqlConnection) {
94
+ throw "Invalid connection configuration. No mysql connection.";
85
95
  }
96
+ const config = {
97
+ host: connection.mysqlConnection.host,
98
+ port: connection.mysqlConnection.port,
99
+ user: connection.mysqlConnection.user,
100
+ password: connection.mysqlConnection.password,
101
+ database: connection.mysqlConnection.database,
102
+ };
103
+ const mysqlConnection = new MySQLConnection(
104
+ connection.name,
105
+ config,
106
+ );
107
+ connectionMap.set(connection.name, mysqlConnection);
108
+ connection.attributes = getConnectionAttributes(mysqlConnection);
109
+ break;
110
+ }
86
111
 
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
- user: 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;
112
+ case "bigquery": {
113
+ if (!connection.bigqueryConnection) {
114
+ throw "Invalid connection configuration. No bigquery connection.";
105
115
  }
106
116
 
107
- case "bigquery": {
108
- if (!connection.bigqueryConnection) {
109
- throw "Invalid connection configuration. No bigquery connection.";
110
- }
111
-
112
- // If a service account key file is provided, we persist it to disk
113
- // and pass the path to the BigQueryConnection.
114
- let serviceAccountKeyPath = undefined;
115
- if (connection.bigqueryConnection.serviceAccountKeyJson) {
116
- serviceAccountKeyPath = path.join(
117
- "/tmp",
118
- `${connection.name}-${uuidv4()}-service-account-key.json`,
119
- );
120
- await fs.writeFile(
121
- serviceAccountKeyPath,
122
- connection.bigqueryConnection
123
- .serviceAccountKeyJson as string,
124
- );
125
- }
126
-
127
- const bigqueryConnectionOptions = {
128
- projectId: connection.bigqueryConnection.defaultProjectId,
129
- serviceAccountKeyPath: serviceAccountKeyPath,
130
- location: connection.bigqueryConnection.location,
131
- maximumBytesBilled:
132
- connection.bigqueryConnection.maximumBytesBilled,
133
- timeoutMs:
134
- connection.bigqueryConnection.queryTimeoutMilliseconds,
135
- billingProjectId:
136
- connection.bigqueryConnection.billingProjectId,
137
- };
138
- const bigqueryConnection = new BigQueryConnection(
139
- connection.name,
140
- () => ({}),
141
- bigqueryConnectionOptions,
117
+ // If a service account key file is provided, we persist it to disk
118
+ // and pass the path to the BigQueryConnection.
119
+ let serviceAccountKeyPath = undefined;
120
+ if (connection.bigqueryConnection.serviceAccountKeyJson) {
121
+ serviceAccountKeyPath = path.join(
122
+ "/tmp",
123
+ `${connection.name}-${uuidv4()}-service-account-key.json`,
124
+ );
125
+ await fs.writeFile(
126
+ serviceAccountKeyPath,
127
+ connection.bigqueryConnection.serviceAccountKeyJson as string,
142
128
  );
143
- connectionMap.set(connection.name, bigqueryConnection);
144
- connection.attributes =
145
- getConnectionAttributes(bigqueryConnection);
146
- break;
147
129
  }
148
130
 
149
- case "snowflake": {
150
- if (!connection.snowflakeConnection) {
151
- throw new Error(
152
- "Snowflake connection configuration is missing.",
153
- );
154
- }
155
- if (!connection.snowflakeConnection.account) {
156
- throw new Error("Snowflake account is required.");
157
- }
158
-
159
- if (!connection.snowflakeConnection.username) {
160
- throw new Error("Snowflake username is required.");
161
- }
131
+ const bigqueryConnectionOptions = {
132
+ projectId: connection.bigqueryConnection.defaultProjectId,
133
+ serviceAccountKeyPath: serviceAccountKeyPath,
134
+ location: connection.bigqueryConnection.location,
135
+ maximumBytesBilled:
136
+ connection.bigqueryConnection.maximumBytesBilled,
137
+ timeoutMs:
138
+ connection.bigqueryConnection.queryTimeoutMilliseconds,
139
+ billingProjectId: connection.bigqueryConnection.billingProjectId,
140
+ };
141
+ const bigqueryConnection = new BigQueryConnection(
142
+ connection.name,
143
+ () => ({}),
144
+ bigqueryConnectionOptions,
145
+ );
146
+ connectionMap.set(connection.name, bigqueryConnection);
147
+ connection.attributes = getConnectionAttributes(bigqueryConnection);
148
+ break;
149
+ }
162
150
 
163
- if (!connection.snowflakeConnection.password) {
164
- throw new Error("Snowflake password is required.");
165
- }
151
+ case "snowflake": {
152
+ if (!connection.snowflakeConnection) {
153
+ throw new Error(
154
+ "Snowflake connection configuration is missing.",
155
+ );
156
+ }
157
+ if (!connection.snowflakeConnection.account) {
158
+ throw new Error("Snowflake account is required.");
159
+ }
166
160
 
167
- if (!connection.snowflakeConnection.warehouse) {
168
- throw new Error("Snowflake warehouse is required.");
169
- }
161
+ if (!connection.snowflakeConnection.username) {
162
+ throw new Error("Snowflake username is required.");
163
+ }
170
164
 
171
- const snowflakeConnectionOptions = {
172
- connOptions: {
173
- account: connection.snowflakeConnection.account,
174
- username: connection.snowflakeConnection.username,
175
- password: connection.snowflakeConnection.password,
176
- warehouse: connection.snowflakeConnection.warehouse,
177
- database: connection.snowflakeConnection.database,
178
- schema: connection.snowflakeConnection.schema,
179
- timeout:
180
- connection.snowflakeConnection
181
- .responseTimeoutMilliseconds,
182
- },
183
- };
184
- const snowflakeConnection = new SnowflakeConnection(
185
- connection.name,
186
- snowflakeConnectionOptions,
187
- );
188
- connectionMap.set(connection.name, snowflakeConnection);
189
- connection.attributes =
190
- getConnectionAttributes(snowflakeConnection);
191
- break;
165
+ if (!connection.snowflakeConnection.password) {
166
+ throw new Error("Snowflake password is required.");
192
167
  }
193
168
 
194
- case "trino": {
195
- if (!connection.trinoConnection) {
196
- throw new Error("Trino connection configuration is missing.");
197
- }
198
- const trinoConnectionOptions = {
199
- server: connection.trinoConnection.server,
200
- port: connection.trinoConnection.port,
201
- catalog: connection.trinoConnection.catalog,
202
- schema: connection.trinoConnection.schema,
203
- user: connection.trinoConnection.user,
204
- password: connection.trinoConnection.password,
205
- };
206
- const trinoConnection = new TrinoConnection(
207
- connection.name,
208
- {},
209
- trinoConnectionOptions,
210
- );
211
- connectionMap.set(connection.name, trinoConnection);
212
- connection.attributes = getConnectionAttributes(trinoConnection);
213
- break;
169
+ if (!connection.snowflakeConnection.warehouse) {
170
+ throw new Error("Snowflake warehouse is required.");
214
171
  }
215
172
 
216
- default: {
217
- throw new Error(
218
- `Unsupported connection type: ${connection.type}`,
219
- );
173
+ const snowflakeConnectionOptions = {
174
+ connOptions: {
175
+ account: connection.snowflakeConnection.account,
176
+ username: connection.snowflakeConnection.username,
177
+ password: connection.snowflakeConnection.password,
178
+ warehouse: connection.snowflakeConnection.warehouse,
179
+ database: connection.snowflakeConnection.database,
180
+ schema: connection.snowflakeConnection.schema,
181
+ timeout:
182
+ connection.snowflakeConnection.responseTimeoutMilliseconds,
183
+ },
184
+ };
185
+ const snowflakeConnection = new SnowflakeConnection(
186
+ connection.name,
187
+ snowflakeConnectionOptions,
188
+ );
189
+ connectionMap.set(connection.name, snowflakeConnection);
190
+ connection.attributes =
191
+ getConnectionAttributes(snowflakeConnection);
192
+ break;
193
+ }
194
+
195
+ case "trino": {
196
+ if (!connection.trinoConnection) {
197
+ throw new Error("Trino connection configuration is missing.");
220
198
  }
199
+ const trinoConnectionOptions = {
200
+ server: connection.trinoConnection.server,
201
+ port: connection.trinoConnection.port,
202
+ catalog: connection.trinoConnection.catalog,
203
+ schema: connection.trinoConnection.schema,
204
+ user: connection.trinoConnection.user,
205
+ password: connection.trinoConnection.password,
206
+ };
207
+ const trinoConnection = new TrinoConnection(
208
+ connection.name,
209
+ {},
210
+ trinoConnectionOptions,
211
+ );
212
+ connectionMap.set(connection.name, trinoConnection);
213
+ connection.attributes = getConnectionAttributes(trinoConnection);
214
+ break;
221
215
  }
222
- });
216
+
217
+ default: {
218
+ throw new Error(`Unsupported connection type: ${connection.type}`);
219
+ }
220
+ }
223
221
  }
224
222
 
225
223
  return {
@@ -25,6 +25,11 @@ import { metrics } from "@opentelemetry/api";
25
25
  import * as fs from "fs/promises";
26
26
  import * as path from "path";
27
27
  import { components } from "../api";
28
+ import {
29
+ MODEL_FILE_SUFFIX,
30
+ NOTEBOOK_FILE_SUFFIX,
31
+ ROW_LIMIT,
32
+ } from "../constants";
28
33
  import { HackyDataStylesAccumulator } from "../data_styles";
29
34
  import {
30
35
  BadRequestError,
@@ -32,12 +37,7 @@ import {
32
37
  ModelNotFoundError,
33
38
  } from "../errors";
34
39
  import { logger } from "../logger";
35
- import {
36
- MODEL_FILE_SUFFIX,
37
- NOTEBOOK_FILE_SUFFIX,
38
- ROW_LIMIT,
39
- URL_READER,
40
- } from "../utils";
40
+ import { URL_READER } from "../utils";
41
41
 
42
42
  type ApiCompiledModel = components["schemas"]["CompiledModel"];
43
43
  type ApiNotebookCell = components["schemas"]["NotebookCell"];
@@ -4,13 +4,18 @@ import { join } from "path";
4
4
  import sinon from "sinon";
5
5
  import { PackageNotFoundError } from "../errors";
6
6
  import { readConnectionConfig } from "./connection";
7
- import { Model } from "./model";
7
+ import { ApiConnection, Model } from "./model";
8
8
  import { Package } from "./package";
9
9
  import { Scheduler } from "./scheduler";
10
10
 
11
11
  // Minimal partial types for mocking
12
12
  type PartialScheduler = Pick<Scheduler, "list">;
13
13
 
14
+ const connectionMocks: ApiConnection[] = [
15
+ { name: "conn1", type: "postgres", postgresConnection: {} },
16
+ { name: "conn2", type: "bigquery", bigqueryConnection: {} },
17
+ ];
18
+
14
19
  describe("service/package", () => {
15
20
  const testPackageDirectory = "testPackage";
16
21
 
@@ -24,10 +29,7 @@ describe("service/package", () => {
24
29
  join(testPackageDirectory, "database.csv"),
25
30
  parquetBuffer,
26
31
  );
27
- const content = JSON.stringify([
28
- { name: "conn1", type: "database" },
29
- { name: "conn2", type: "api" },
30
- ]);
32
+ const content = JSON.stringify(connectionMocks);
31
33
  await fs.writeFile(
32
34
  join(testPackageDirectory, "publisher.connections.json"),
33
35
  content,
@@ -82,7 +84,7 @@ describe("service/package", () => {
82
84
  });
83
85
  it("should return a Package object if the package exists", async () => {
84
86
  sinon.stub(fs, "stat").resolves();
85
- sinon
87
+ const readFileStub = sinon
86
88
  .stub(fs, "readFile")
87
89
  .resolves(
88
90
  Buffer.from(JSON.stringify({ description: "Test package" })),
@@ -98,6 +100,9 @@ describe("service/package", () => {
98
100
  list: () => [],
99
101
  } as PartialScheduler);
100
102
 
103
+ readFileStub.restore();
104
+ readFileStub.resolves(Buffer.from(JSON.stringify([])));
105
+
101
106
  const packageInstance = await Package.create(
102
107
  "testProject",
103
108
  "testPackage",
@@ -221,10 +226,7 @@ describe("service/package", () => {
221
226
  sinon.stub(fs, "stat").resolves();
222
227
  const config = await readConnectionConfig(testPackageDirectory);
223
228
 
224
- expect(config).toEqual([
225
- { name: "conn1", type: "database" },
226
- { name: "conn2", type: "api" },
227
- ]);
229
+ expect(config).toEqual(connectionMocks);
228
230
  });
229
231
  });
230
232
  });
@@ -11,14 +11,14 @@ import {
11
11
  import { metrics } from "@opentelemetry/api";
12
12
  import recursive from "recursive-readdir";
13
13
  import { components } from "../api";
14
- import { API_PREFIX } from "../constants";
15
- import { PackageNotFoundError } from "../errors";
16
- import { logger } from "../logger";
17
14
  import {
15
+ API_PREFIX,
18
16
  MODEL_FILE_SUFFIX,
19
17
  NOTEBOOK_FILE_SUFFIX,
20
18
  PACKAGE_MANIFEST_NAME,
21
- } from "../utils";
19
+ } from "../constants";
20
+ import { PackageNotFoundError } from "../errors";
21
+ import { logger } from "../logger";
22
22
  import { createConnections } from "./connection";
23
23
  import { Model } from "./model";
24
24
  import { Scheduler } from "./scheduler";
@@ -101,7 +101,10 @@ export class Package {
101
101
  unit: "ms",
102
102
  });
103
103
  const connections = new Map<string, Connection>(projectConnections);
104
-
104
+ logger.info(`Project connections: ${connections.size}`, {
105
+ connections,
106
+ projectConnections,
107
+ });
105
108
  // Package connections override project connections.
106
109
  const { malloyConnections: packageConnections } =
107
110
  await createConnections(packagePath);
@@ -147,6 +150,11 @@ export class Package {
147
150
  malloy_package_name: packageName,
148
151
  status: "success",
149
152
  });
153
+ logger.info(`Successfully loaded package ${packageName}`, {
154
+ packageName,
155
+ duration: executionTime,
156
+ unit: "ms",
157
+ });
150
158
  return new Package(
151
159
  projectName,
152
160
  packageName,
@@ -158,15 +166,14 @@ export class Package {
158
166
  );
159
167
  } catch (error) {
160
168
  logger.error(`Error loading package ${packageName}`, { error });
169
+ console.error(error);
161
170
  const endTime = performance.now();
162
171
  const executionTime = endTime - startTime;
163
172
  this.packageLoadHistogram.record(executionTime, {
164
173
  malloy_package_name: packageName,
165
174
  status: "error",
166
175
  });
167
- throw new Error(`Error loading package ${packageName}`, {
168
- cause: error,
169
- });
176
+ throw error;
170
177
  }
171
178
  }
172
179