@malloy-publisher/server 0.0.382-dev → 0.2.1
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/README.docker.md +145 -20
- package/README.md +65 -12
- package/dist/app/api-doc.html +5 -0
- package/dist/app/api-doc.yaml +3929 -534
- package/dist/app/assets/EnvironmentPage-CHq5BF4l.js +1 -0
- package/dist/app/assets/HomePage-CIp-pJCa.js +1 -0
- package/dist/app/assets/LightMode-mHHPjEeA.js +1 -0
- package/dist/app/assets/MainPage-CylxO5Yu.js +2 -0
- package/dist/app/assets/MaterializationsPage-a0vjqxib.js +1 -0
- package/dist/app/assets/ModelPage-Cz-MrSZQ.js +1 -0
- package/dist/app/assets/PackagePage-Cdg5D7AN.js +1 -0
- package/dist/app/assets/RouteError-B5SIyn6G.js +1 -0
- package/dist/app/assets/ThemeEditorPage-B62rRc-y.js +1 -0
- package/dist/app/assets/WorkbookPage-45cG1Gx6.js +1 -0
- package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-DyU47reh.es-DY9b3WjS.js} +1 -1
- package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
- package/dist/app/assets/index-5eLCcNmP.css +1 -0
- package/dist/app/assets/index-C5ClsnS7.js +1774 -0
- package/dist/app/assets/index-DF9tVqwN.js +1 -0
- package/dist/app/assets/index-DSoCUF-f.js +2725 -0
- package/dist/app/assets/index-DgVRz2wm.js +2 -0
- package/dist/app/assets/index-P-pF8nU5.js +632 -0
- package/dist/app/assets/index-w_0OQJgZ.js +23 -0
- package/dist/app/index.html +8 -4
- package/dist/cpufeatures-1yrn0vtw.node +0 -0
- package/dist/default-publisher.config.json +23 -0
- package/dist/{instrumentation.js → instrumentation.mjs} +10631 -10573
- package/dist/package_load_worker.mjs +15219 -0
- package/dist/runtime/publisher.js +331 -0
- package/dist/{server.js → server.mjs} +141276 -74705
- package/dist/sshcrypto-vd2k5hq9.node +0 -0
- package/package.json +41 -28
- package/publisher.config.example.bigquery.json +33 -0
- package/publisher.config.example.duckdb.json +23 -0
- package/.eslintrc.json +0 -44
- package/.prettierignore +0 -1
- package/.prettierrc +0 -4
- package/build.ts +0 -38
- package/dist/app/assets/HomePage-Dn3E4CuB.js +0 -1
- package/dist/app/assets/MainPage-BzB3yoqi.js +0 -2
- package/dist/app/assets/ModelPage-C9O_sAXT.js +0 -1
- package/dist/app/assets/PackagePage-DcxKEjBX.js +0 -1
- package/dist/app/assets/ProjectPage-BDj307rF.js +0 -1
- package/dist/app/assets/RouteError-DAShbVCG.js +0 -1
- package/dist/app/assets/WorkbookPage-Cs_XYEaB.js +0 -1
- package/dist/app/assets/index-15BOvhp0.js +0 -456
- package/dist/app/assets/index-Bb2jqquW.js +0 -676
- package/dist/app/assets/index-CMlGQMcl.css +0 -1
- package/dist/app/assets/index-D68X76-7.js +0 -1276
- package/dist/app/assets/index.umd-DGBekgSu.js +0 -1145
- package/dxt/malloy_bridge.py +0 -354
- package/dxt/manifest.json +0 -22
- package/eslint.config.mjs +0 -76
- package/k6-tests/README.md +0 -144
- package/k6-tests/bun.lock +0 -797
- package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
- package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
- package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
- package/k6-tests/load-test/load-test-crud.ts +0 -142
- package/k6-tests/load-test/load-test.ts +0 -784
- package/k6-tests/package.json +0 -27
- package/k6-tests/scripts/run-with-otel.sh +0 -50
- package/k6-tests/smoke-test/smoke-test.ts +0 -158
- package/k6-tests/tsconfig.json +0 -29
- package/k6-tests/utils/client_factory.ts +0 -128
- package/k6-tests/utils/common.ts +0 -489
- package/k6-tests/utils/logger.ts +0 -62
- package/k6-tests/utils/types.d.ts +0 -35
- package/malloy_mcp.dxt +0 -0
- package/publisher.config.json +0 -33
- package/src/config.spec.ts +0 -850
- package/src/config.ts +0 -305
- package/src/constants.ts +0 -10
- package/src/controller/compile.controller.ts +0 -35
- package/src/controller/connection.controller.ts +0 -515
- package/src/controller/database.controller.ts +0 -21
- package/src/controller/manifest.controller.ts +0 -29
- package/src/controller/materialization.controller.ts +0 -125
- package/src/controller/model.controller.ts +0 -107
- package/src/controller/package.controller.ts +0 -182
- package/src/controller/query.controller.ts +0 -60
- package/src/controller/watch-mode.controller.ts +0 -98
- package/src/data_styles.ts +0 -71
- package/src/dto/connection.dto.spec.ts +0 -131
- package/src/dto/connection.dto.ts +0 -174
- package/src/dto/index.ts +0 -2
- package/src/dto/package.dto.spec.ts +0 -42
- package/src/dto/package.dto.ts +0 -12
- package/src/dto/validate.spec.ts +0 -76
- package/src/dto/validate.ts +0 -31
- package/src/errors.ts +0 -116
- package/src/health.ts +0 -189
- package/src/instrumentation.ts +0 -148
- package/src/logger.ts +0 -154
- package/src/mcp/error_messages.ts +0 -199
- package/src/mcp/handler_utils.ts +0 -239
- package/src/mcp/mcp_constants.ts +0 -22
- package/src/mcp/prompts/handlers.ts +0 -84
- package/src/mcp/prompts/index.ts +0 -11
- package/src/mcp/prompts/prompt_definitions.ts +0 -160
- package/src/mcp/prompts/prompt_service.ts +0 -67
- package/src/mcp/prompts/utils.ts +0 -62
- package/src/mcp/resource_metadata.ts +0 -47
- package/src/mcp/resources/model_resource.ts +0 -153
- package/src/mcp/resources/notebook_resource.ts +0 -137
- package/src/mcp/resources/package_resource.ts +0 -370
- package/src/mcp/resources/project_resource.ts +0 -184
- package/src/mcp/resources/query_resource.ts +0 -117
- package/src/mcp/resources/source_resource.ts +0 -141
- package/src/mcp/resources/view_resource.ts +0 -136
- package/src/mcp/server.ts +0 -62
- package/src/mcp/tools/discovery_tools.ts +0 -258
- package/src/mcp/tools/execute_query_tool.ts +0 -265
- package/src/server.ts +0 -1195
- package/src/service/connection.spec.ts +0 -1331
- package/src/service/connection.ts +0 -1511
- package/src/service/connection_service.spec.ts +0 -641
- package/src/service/connection_service.ts +0 -181
- package/src/service/db_utils.spec.ts +0 -714
- package/src/service/db_utils.ts +0 -1227
- package/src/service/filter.spec.ts +0 -447
- package/src/service/filter.ts +0 -337
- package/src/service/filter_integration.spec.ts +0 -825
- package/src/service/gcs_s3_utils.ts +0 -427
- package/src/service/manifest_service.spec.ts +0 -201
- package/src/service/manifest_service.ts +0 -106
- package/src/service/materialization_service.spec.ts +0 -648
- package/src/service/materialization_service.ts +0 -929
- package/src/service/materialized_table_gc.spec.ts +0 -383
- package/src/service/materialized_table_gc.ts +0 -279
- package/src/service/model.spec.ts +0 -257
- package/src/service/model.ts +0 -1069
- package/src/service/package.spec.ts +0 -236
- package/src/service/package.ts +0 -463
- package/src/service/project.ts +0 -681
- package/src/service/project_compile.spec.ts +0 -197
- package/src/service/project_store.spec.ts +0 -859
- package/src/service/project_store.ts +0 -1531
- package/src/service/quoting.ts +0 -41
- package/src/service/resolve_project.ts +0 -13
- package/src/storage/BaseRepository.ts +0 -31
- package/src/storage/DatabaseInterface.ts +0 -180
- package/src/storage/StorageManager.mock.ts +0 -59
- package/src/storage/StorageManager.ts +0 -210
- package/src/storage/duckdb/ConnectionRepository.ts +0 -141
- package/src/storage/duckdb/DuckDBConnection.ts +0 -177
- package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
- package/src/storage/duckdb/DuckDBRepository.ts +0 -224
- package/src/storage/duckdb/ManifestRepository.ts +0 -119
- package/src/storage/duckdb/MaterializationRepository.ts +0 -249
- package/src/storage/duckdb/PackageRepository.ts +0 -135
- package/src/storage/duckdb/ProjectRepository.ts +0 -148
- package/src/storage/duckdb/manifest_store.spec.ts +0 -133
- package/src/storage/duckdb/schema.ts +0 -147
- package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -146
- package/src/utils.ts +0 -13
- package/tests/fixtures/persist-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-test/persist_test.malloy +0 -11
- package/tests/fixtures/persist-test/publisher.json +0 -5
- package/tests/fixtures/publisher.config.json +0 -15
- package/tests/harness/e2e.ts +0 -96
- package/tests/harness/mcp_test_setup.ts +0 -344
- package/tests/harness/mocks.ts +0 -37
- package/tests/harness/rest_e2e.ts +0 -68
- package/tests/harness/uris.ts +0 -31
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -470
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -434
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -663
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -122
- package/tests/integration/mcp/setup.spec.ts +0 -5
- package/tests/unit/duckdb/attached_databases.test.ts +0 -1132
- package/tests/unit/ducklake/ducklake.test.ts +0 -950
- package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
- package/tests/unit/mcp/prompt_happy.test.ts +0 -51
- package/tsconfig.json +0 -32
|
@@ -1,515 +0,0 @@
|
|
|
1
|
-
import { Connection, RunSQLOptions, TableSourceDef } from "@malloydata/malloy";
|
|
2
|
-
import { PersistSQLResults } from "@malloydata/malloy/connection";
|
|
3
|
-
import { components } from "../api";
|
|
4
|
-
import { BadRequestError, ConnectionError } from "../errors";
|
|
5
|
-
import { logger } from "../logger";
|
|
6
|
-
import { testConnectionConfig } from "../service/connection";
|
|
7
|
-
import { ConnectionService } from "../service/connection_service";
|
|
8
|
-
import {
|
|
9
|
-
getSchemasForConnection,
|
|
10
|
-
listTablesForSchema,
|
|
11
|
-
} from "../service/db_utils";
|
|
12
|
-
import { ProjectStore } from "../service/project_store";
|
|
13
|
-
|
|
14
|
-
type ApiConnection = components["schemas"]["Connection"];
|
|
15
|
-
type ApiConnectionStatus = components["schemas"]["ConnectionStatus"];
|
|
16
|
-
type ApiSqlSource = components["schemas"]["SqlSource"];
|
|
17
|
-
type ApiTable = components["schemas"]["Table"];
|
|
18
|
-
type ApiQueryData = components["schemas"]["QueryData"];
|
|
19
|
-
type ApiTemporaryTable = components["schemas"]["TemporaryTable"];
|
|
20
|
-
type ApiSchema = components["schemas"]["Schema"];
|
|
21
|
-
const AZURE_SUPPORTED_SCHEMES = ["https://", "http://", "abfss://", "az://"];
|
|
22
|
-
const AZURE_DATA_EXTENSIONS = [
|
|
23
|
-
".parquet",
|
|
24
|
-
".csv",
|
|
25
|
-
".json",
|
|
26
|
-
".jsonl",
|
|
27
|
-
".ndjson",
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Validates an Azure URL against the three supported patterns:
|
|
32
|
-
* 1. Single file: path/file.parquet
|
|
33
|
-
* 2. Directory glob: path/*.ext (direct children only, no sub-dirs)
|
|
34
|
-
* 3. Recursive: path/** (all data files recursively)
|
|
35
|
-
*/
|
|
36
|
-
function validateAzureUrl(url: string, fieldName: string): void {
|
|
37
|
-
if (!AZURE_SUPPORTED_SCHEMES.some((s) => url.startsWith(s))) {
|
|
38
|
-
throw new BadRequestError(
|
|
39
|
-
`Azure ${fieldName} must use one of: ${AZURE_SUPPORTED_SCHEMES.join(", ")}`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const pathWithoutQuery = url.split("?")[0];
|
|
44
|
-
const stars = (pathWithoutQuery.match(/\*/g) || []).length;
|
|
45
|
-
|
|
46
|
-
if (stars === 0) {
|
|
47
|
-
// Single file — must end with a data extension
|
|
48
|
-
const lower = pathWithoutQuery.toLowerCase();
|
|
49
|
-
if (!AZURE_DATA_EXTENSIONS.some((ext) => lower.endsWith(ext))) {
|
|
50
|
-
throw new BadRequestError(
|
|
51
|
-
`Azure ${fieldName}: a single-file URL must end with a data file extension (${AZURE_DATA_EXTENSIONS.join(", ")})`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
} else if (pathWithoutQuery.endsWith("**")) {
|
|
55
|
-
// Recursive — valid, no further checks needed
|
|
56
|
-
} else {
|
|
57
|
-
// Must be exactly path/*.ext — one star, in the last path segment only
|
|
58
|
-
const lastSegment = pathWithoutQuery.split("/").pop() || "";
|
|
59
|
-
if (stars !== 1 || !lastSegment.startsWith("*")) {
|
|
60
|
-
throw new BadRequestError(
|
|
61
|
-
`Azure ${fieldName}: only three URL patterns are supported:\n` +
|
|
62
|
-
` • Single file: path/file.parquet\n` +
|
|
63
|
-
` • Directory glob: path/*.ext (direct children only)\n` +
|
|
64
|
-
` • Recursive: path/** (all data files in subtree)\n` +
|
|
65
|
-
`Multi-level globs such as "sub_dir/*/*.parquet" are not supported.`,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function validateAzureAttachedDatabases(connectionConfig: ApiConnection): void {
|
|
72
|
-
if (connectionConfig.type !== "duckdb") return;
|
|
73
|
-
const attachedDbs =
|
|
74
|
-
connectionConfig.duckdbConnection?.attachedDatabases || [];
|
|
75
|
-
for (const db of attachedDbs) {
|
|
76
|
-
if (db.type !== "azure" || !db.azureConnection) continue;
|
|
77
|
-
const { authType, sasUrl, fileUrl } = db.azureConnection;
|
|
78
|
-
if (authType === "sas_token" && sasUrl) {
|
|
79
|
-
validateAzureUrl(sasUrl, `"${db.name}" sasUrl`);
|
|
80
|
-
} else if (authType === "service_principal" && fileUrl) {
|
|
81
|
-
validateAzureUrl(fileUrl, `"${db.name}" fileUrl`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export class ConnectionController {
|
|
87
|
-
private projectStore: ProjectStore;
|
|
88
|
-
private connectionService: ConnectionService;
|
|
89
|
-
constructor(projectStore: ProjectStore) {
|
|
90
|
-
this.projectStore = projectStore;
|
|
91
|
-
this.connectionService = new ConnectionService(projectStore);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Gets the appropriate Malloy connection for a given connection name.
|
|
96
|
-
* For DuckDB connections, retrieves from package level; for others, from project level.
|
|
97
|
-
*/
|
|
98
|
-
private async getMalloyConnection(
|
|
99
|
-
projectName: string,
|
|
100
|
-
connectionName: string,
|
|
101
|
-
): Promise<Connection> {
|
|
102
|
-
const project = await this.projectStore.getProject(projectName, false);
|
|
103
|
-
const connection = project.getApiConnection(connectionName);
|
|
104
|
-
|
|
105
|
-
// For DuckDB connections, get the connection from a package
|
|
106
|
-
if (connection.name === "duckdb" && connection.type === "duckdb") {
|
|
107
|
-
const packages = await project.listPackages();
|
|
108
|
-
if (packages.length === 0) {
|
|
109
|
-
return project.getMalloyConnection(connectionName);
|
|
110
|
-
}
|
|
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");
|
|
115
|
-
}
|
|
116
|
-
const pkg = await project.getPackage(packageName);
|
|
117
|
-
return pkg.getMalloyConnection(connectionName);
|
|
118
|
-
} else {
|
|
119
|
-
return project.getMalloyConnection(connectionName);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Fetches a table's schema via the Malloy connection's fetchTableSchema,
|
|
125
|
-
* returning an ApiTable with columns and the raw source JSON.
|
|
126
|
-
*/
|
|
127
|
-
private async fetchTable(
|
|
128
|
-
malloyConnection: Connection,
|
|
129
|
-
tableKey: string,
|
|
130
|
-
tablePath: string,
|
|
131
|
-
): Promise<ApiTable> {
|
|
132
|
-
try {
|
|
133
|
-
const source = await (
|
|
134
|
-
malloyConnection as Connection & {
|
|
135
|
-
fetchTableSchema: (
|
|
136
|
-
tableKey: string,
|
|
137
|
-
tablePath: string,
|
|
138
|
-
) => Promise<TableSourceDef | undefined>;
|
|
139
|
-
}
|
|
140
|
-
).fetchTableSchema(tableKey, tablePath);
|
|
141
|
-
if (!source) {
|
|
142
|
-
throw new ConnectionError(`Table ${tablePath} not found`);
|
|
143
|
-
}
|
|
144
|
-
// BigQueryConnection returns `error.message` as a string on failure instead of throwing.
|
|
145
|
-
if (typeof source === "string") {
|
|
146
|
-
throw new ConnectionError(source);
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
source: JSON.stringify(source),
|
|
150
|
-
resource: tablePath,
|
|
151
|
-
columns: (source.fields || []).map((f) => ({
|
|
152
|
-
name: f.name,
|
|
153
|
-
type: f.type,
|
|
154
|
-
})),
|
|
155
|
-
};
|
|
156
|
-
} catch (error) {
|
|
157
|
-
const errorMessage =
|
|
158
|
-
error instanceof Error
|
|
159
|
-
? error.message
|
|
160
|
-
: typeof error === "string"
|
|
161
|
-
? error
|
|
162
|
-
: JSON.stringify(error);
|
|
163
|
-
logger.error("fetchTableSchema error", {
|
|
164
|
-
error,
|
|
165
|
-
tableKey,
|
|
166
|
-
tablePath,
|
|
167
|
-
});
|
|
168
|
-
throw new ConnectionError(errorMessage);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
public async getConnection(
|
|
173
|
-
projectName: string,
|
|
174
|
-
connectionName: string,
|
|
175
|
-
): Promise<ApiConnection> {
|
|
176
|
-
if (!projectName || !connectionName) {
|
|
177
|
-
throw new BadRequestError("Connection payload is required");
|
|
178
|
-
}
|
|
179
|
-
// Prefer the in-memory API connection (which was materialized by the
|
|
180
|
-
// project on load and carries `attributes`). The DB row stores the
|
|
181
|
-
// raw config and FK columns, which aren't the ApiConnection shape.
|
|
182
|
-
const project = await this.projectStore.getProject(projectName, false);
|
|
183
|
-
return project.getApiConnection(connectionName);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
public async listConnections(projectName: string): Promise<ApiConnection[]> {
|
|
187
|
-
const project = await this.projectStore.getProject(projectName, false);
|
|
188
|
-
return project.listApiConnections();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Lists schemas (namespaces) available in a connection
|
|
192
|
-
public async listSchemas(
|
|
193
|
-
projectName: string,
|
|
194
|
-
connectionName: string,
|
|
195
|
-
): Promise<ApiSchema[]> {
|
|
196
|
-
const project = await this.projectStore.getProject(projectName, false);
|
|
197
|
-
const connection = project.getApiConnection(connectionName);
|
|
198
|
-
const malloyConnection = await this.getMalloyConnection(
|
|
199
|
-
projectName,
|
|
200
|
-
connectionName,
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
return getSchemasForConnection(connection, malloyConnection);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Lists tables available in a schema. For postgres the schema is usually "public"
|
|
207
|
-
public async listTables(
|
|
208
|
-
projectName: string,
|
|
209
|
-
connectionName: string,
|
|
210
|
-
schemaName: string,
|
|
211
|
-
tableNames?: string[],
|
|
212
|
-
): Promise<ApiTable[]> {
|
|
213
|
-
const project = await this.projectStore.getProject(projectName, false);
|
|
214
|
-
const connection = project.getApiConnection(connectionName);
|
|
215
|
-
const malloyConnection = await this.getMalloyConnection(
|
|
216
|
-
projectName,
|
|
217
|
-
connectionName,
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
return listTablesForSchema(
|
|
221
|
-
connection,
|
|
222
|
-
schemaName,
|
|
223
|
-
malloyConnection,
|
|
224
|
-
tableNames,
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
public async getConnectionSqlSource(
|
|
229
|
-
projectName: string,
|
|
230
|
-
connectionName: string,
|
|
231
|
-
sqlStatement: string,
|
|
232
|
-
): Promise<ApiSqlSource> {
|
|
233
|
-
const malloyConnection = await this.getMalloyConnection(
|
|
234
|
-
projectName,
|
|
235
|
-
connectionName,
|
|
236
|
-
);
|
|
237
|
-
try {
|
|
238
|
-
const schema = await (
|
|
239
|
-
malloyConnection as Connection & {
|
|
240
|
-
fetchSelectSchema: (params: {
|
|
241
|
-
connection: string;
|
|
242
|
-
selectStr: string;
|
|
243
|
-
}) => Promise<unknown>;
|
|
244
|
-
}
|
|
245
|
-
).fetchSelectSchema({
|
|
246
|
-
connection: connectionName,
|
|
247
|
-
selectStr: sqlStatement,
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// BigQueryConnection returns `error.message` as a string on failure instead of throwing.
|
|
251
|
-
if (typeof schema === "string") {
|
|
252
|
-
throw new ConnectionError(schema);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return {
|
|
256
|
-
source: JSON.stringify(schema),
|
|
257
|
-
};
|
|
258
|
-
} catch (error) {
|
|
259
|
-
throw new ConnectionError((error as Error).message);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
public async getTable(
|
|
264
|
-
projectName: string,
|
|
265
|
-
connectionName: string,
|
|
266
|
-
schemaName: string,
|
|
267
|
-
tablePath: string,
|
|
268
|
-
): Promise<ApiTable> {
|
|
269
|
-
const malloyConnection = await this.getMalloyConnection(
|
|
270
|
-
projectName,
|
|
271
|
-
connectionName,
|
|
272
|
-
);
|
|
273
|
-
// Use getApiConnection to get the unwrapped ApiConnection config, consistent with listSchemas and listTables.
|
|
274
|
-
const project = await this.projectStore.getProject(projectName, false);
|
|
275
|
-
const connection = project.getApiConnection(connectionName);
|
|
276
|
-
|
|
277
|
-
// TODO: Move this database connection logic to the db_utils.ts file -- and
|
|
278
|
-
// ultimately into a connection-specific class.
|
|
279
|
-
if (connection.type === "ducklake") {
|
|
280
|
-
if (tablePath.split(".").length === 1) {
|
|
281
|
-
// tablePath is just the table name, construct full path
|
|
282
|
-
tablePath = `${connectionName}.${schemaName}.${tablePath}`;
|
|
283
|
-
} else if (
|
|
284
|
-
tablePath.split(".").length === 2 &&
|
|
285
|
-
!tablePath.startsWith(connectionName)
|
|
286
|
-
) {
|
|
287
|
-
// tablePath is schemaName.tableName but missing connection prefix
|
|
288
|
-
tablePath = `${connectionName}.${tablePath}`;
|
|
289
|
-
}
|
|
290
|
-
// If tablePath already has 3+ parts or starts with connection name, use as-is
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Check if this is an Azure attached database
|
|
294
|
-
if (connection.type === "duckdb") {
|
|
295
|
-
const attachedDbs =
|
|
296
|
-
connection.duckdbConnection?.attachedDatabases || [];
|
|
297
|
-
const azureDb = attachedDbs.find(
|
|
298
|
-
(db) =>
|
|
299
|
-
db.type === "azure" &&
|
|
300
|
-
db.name === schemaName &&
|
|
301
|
-
db.azureConnection,
|
|
302
|
-
);
|
|
303
|
-
if (azureDb && azureDb.azureConnection) {
|
|
304
|
-
// Reconstruct the full SAS URL for the specific file
|
|
305
|
-
const azureConn = azureDb.azureConnection;
|
|
306
|
-
const baseUrl =
|
|
307
|
-
azureConn.authType === "sas_token"
|
|
308
|
-
? azureConn.sasUrl
|
|
309
|
-
: azureConn.fileUrl;
|
|
310
|
-
if (baseUrl) {
|
|
311
|
-
// Extract the file name from tablePath (e.g., "a.aircraft.parquet" -> "aircraft.parquet")
|
|
312
|
-
const fileName = tablePath.includes(".")
|
|
313
|
-
? tablePath.split(".").slice(1).join(".")
|
|
314
|
-
: tablePath;
|
|
315
|
-
// Replace the file portion in the base URL with the specific file name
|
|
316
|
-
const urlParts = baseUrl.split("?");
|
|
317
|
-
const basePath = urlParts[0];
|
|
318
|
-
const queryString = urlParts[1] ? `?${urlParts[1]}` : "";
|
|
319
|
-
// Replace the last path segment (or glob) with the actual file name
|
|
320
|
-
const dirPath = basePath.substring(
|
|
321
|
-
0,
|
|
322
|
-
basePath.lastIndexOf("/") + 1,
|
|
323
|
-
);
|
|
324
|
-
const fullFileUrl = `${dirPath}${fileName}${queryString}`;
|
|
325
|
-
|
|
326
|
-
const table = await this.fetchTable(
|
|
327
|
-
malloyConnection,
|
|
328
|
-
fileName,
|
|
329
|
-
fullFileUrl,
|
|
330
|
-
);
|
|
331
|
-
return { ...table, resource: tablePath };
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const tableKey = tablePath.split(".").pop();
|
|
337
|
-
if (!tableKey) {
|
|
338
|
-
throw new Error(`Invalid tablePath: ${tablePath}`);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return this.fetchTable(malloyConnection, tableKey, tablePath);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
public async getConnectionQueryData(
|
|
345
|
-
projectName: string,
|
|
346
|
-
connectionName: string,
|
|
347
|
-
sqlStatement: string,
|
|
348
|
-
options: string,
|
|
349
|
-
): Promise<ApiQueryData> {
|
|
350
|
-
const malloyConnection = await this.getMalloyConnection(
|
|
351
|
-
projectName,
|
|
352
|
-
connectionName,
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
let runSQLOptions: RunSQLOptions = {};
|
|
356
|
-
if (options) {
|
|
357
|
-
runSQLOptions = JSON.parse(options) as RunSQLOptions;
|
|
358
|
-
}
|
|
359
|
-
if (runSQLOptions.abortSignal) {
|
|
360
|
-
// Add support for abortSignal in the future
|
|
361
|
-
logger.info("Clearing unsupported abortSignal");
|
|
362
|
-
runSQLOptions.abortSignal = undefined;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
try {
|
|
366
|
-
return {
|
|
367
|
-
data: JSON.stringify(
|
|
368
|
-
await malloyConnection.runSQL(sqlStatement, runSQLOptions),
|
|
369
|
-
),
|
|
370
|
-
};
|
|
371
|
-
} catch (error) {
|
|
372
|
-
throw new ConnectionError((error as Error).message);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
public async getConnectionTemporaryTable(
|
|
377
|
-
projectName: string,
|
|
378
|
-
connectionName: string,
|
|
379
|
-
sqlStatement: string,
|
|
380
|
-
): Promise<ApiTemporaryTable> {
|
|
381
|
-
const malloyConnection = await this.getMalloyConnection(
|
|
382
|
-
projectName,
|
|
383
|
-
connectionName,
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
try {
|
|
387
|
-
return {
|
|
388
|
-
table: JSON.stringify(
|
|
389
|
-
await (
|
|
390
|
-
malloyConnection as PersistSQLResults
|
|
391
|
-
).manifestTemporaryTable(sqlStatement),
|
|
392
|
-
),
|
|
393
|
-
};
|
|
394
|
-
} catch (error) {
|
|
395
|
-
throw new ConnectionError((error as Error).message);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
public async testConnectionConfiguration(
|
|
400
|
-
connectionConfig: ApiConnection,
|
|
401
|
-
): 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
|
-
}
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
!connectionConfig ||
|
|
414
|
-
typeof connectionConfig !== "object" ||
|
|
415
|
-
Object.keys(connectionConfig).length === 0
|
|
416
|
-
) {
|
|
417
|
-
throw new BadRequestError(
|
|
418
|
-
"Connection configuration is required and cannot be empty",
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (!connectionConfig.type || typeof connectionConfig.type !== "string") {
|
|
423
|
-
throw new BadRequestError(
|
|
424
|
-
"Connection type is required and must be a string",
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
try {
|
|
429
|
-
return await testConnectionConfig(connectionConfig);
|
|
430
|
-
} catch (error) {
|
|
431
|
-
return {
|
|
432
|
-
status: "failed",
|
|
433
|
-
errorMessage: `Connection test failed: ${(error as Error).message}`,
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
public async addConnection(
|
|
439
|
-
projectName: string,
|
|
440
|
-
connectionName: string,
|
|
441
|
-
connectionConfig: ApiConnection,
|
|
442
|
-
): Promise<{ message: string }> {
|
|
443
|
-
if (!connectionConfig || typeof connectionConfig !== "object") {
|
|
444
|
-
throw new BadRequestError("Connection configuration is required");
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (!connectionConfig) {
|
|
448
|
-
throw new BadRequestError("Connection name is required");
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (!connectionConfig.type) {
|
|
452
|
-
throw new BadRequestError("Connection type is required");
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
validateAzureAttachedDatabases(connectionConfig);
|
|
456
|
-
|
|
457
|
-
logger.info(
|
|
458
|
-
`Creating connection "${connectionName}" in project "${projectName}"`,
|
|
459
|
-
);
|
|
460
|
-
|
|
461
|
-
await this.connectionService.addConnection(
|
|
462
|
-
projectName,
|
|
463
|
-
connectionName,
|
|
464
|
-
connectionConfig,
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
return {
|
|
468
|
-
message: `Connection "${connectionName}" created successfully`,
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
public async updateConnection(
|
|
473
|
-
projectName: string,
|
|
474
|
-
connectionName: string,
|
|
475
|
-
connection: Partial<ApiConnection>,
|
|
476
|
-
): Promise<{ message: string }> {
|
|
477
|
-
if (!connection || typeof connection !== "object") {
|
|
478
|
-
throw new BadRequestError("Connection payload is required");
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
validateAzureAttachedDatabases(connection as ApiConnection);
|
|
482
|
-
|
|
483
|
-
logger.info(
|
|
484
|
-
`Updating connection "${connectionName}" in project "${projectName}"`,
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
await this.connectionService.updateConnection(
|
|
488
|
-
projectName,
|
|
489
|
-
connectionName,
|
|
490
|
-
connection,
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
return {
|
|
494
|
-
message: `Connection "${connectionName}" updated successfully`,
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
public async deleteConnection(
|
|
499
|
-
projectName: string,
|
|
500
|
-
connectionName: string,
|
|
501
|
-
): Promise<{ message: string }> {
|
|
502
|
-
logger.info(
|
|
503
|
-
`Deleting connection "${connectionName}" from project "${projectName}"`,
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
await this.connectionService.deleteConnection(
|
|
507
|
-
projectName,
|
|
508
|
-
connectionName,
|
|
509
|
-
);
|
|
510
|
-
|
|
511
|
-
return {
|
|
512
|
-
message: `Connection "${connectionName}" deleted successfully`,
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { components } from "../api";
|
|
2
|
-
import { ProjectStore } from "../service/project_store";
|
|
3
|
-
|
|
4
|
-
type ApiDatabase = components["schemas"]["Database"];
|
|
5
|
-
|
|
6
|
-
export class DatabaseController {
|
|
7
|
-
private projectStore: ProjectStore;
|
|
8
|
-
|
|
9
|
-
constructor(projectStore: ProjectStore) {
|
|
10
|
-
this.projectStore = projectStore;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
public async listDatabases(
|
|
14
|
-
projectName: string,
|
|
15
|
-
packageName: string,
|
|
16
|
-
): Promise<ApiDatabase[]> {
|
|
17
|
-
const project = await this.projectStore.getProject(projectName, false);
|
|
18
|
-
const p = await project.getPackage(packageName, false);
|
|
19
|
-
return p.listDatabases();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { ManifestService } from "../service/manifest_service";
|
|
2
|
-
import { ProjectStore } from "../service/project_store";
|
|
3
|
-
import { resolveProjectId } from "../service/resolve_project";
|
|
4
|
-
|
|
5
|
-
export class ManifestController {
|
|
6
|
-
constructor(
|
|
7
|
-
private projectStore: ProjectStore,
|
|
8
|
-
private manifestService: ManifestService,
|
|
9
|
-
) {}
|
|
10
|
-
|
|
11
|
-
async getManifest(projectName: string, packageName: string) {
|
|
12
|
-
const repository = this.projectStore.storageManager.getRepository();
|
|
13
|
-
const projectId = await resolveProjectId(repository, projectName);
|
|
14
|
-
// Verify the package exists so we return 404 instead of an empty manifest.
|
|
15
|
-
const project = await this.projectStore.getProject(projectName, false);
|
|
16
|
-
await project.getPackage(packageName, false);
|
|
17
|
-
return this.manifestService.getManifest(projectId, packageName);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async reloadManifest(projectName: string, packageName: string) {
|
|
21
|
-
const repository = this.projectStore.storageManager.getRepository();
|
|
22
|
-
const projectId = await resolveProjectId(repository, projectName);
|
|
23
|
-
return this.manifestService.reloadManifest(
|
|
24
|
-
projectId,
|
|
25
|
-
packageName,
|
|
26
|
-
projectName,
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { BadRequestError } from "../errors";
|
|
2
|
-
import { MaterializationService } from "../service/materialization_service";
|
|
3
|
-
|
|
4
|
-
export class MaterializationController {
|
|
5
|
-
constructor(private materializationService: MaterializationService) {}
|
|
6
|
-
|
|
7
|
-
async createMaterialization(
|
|
8
|
-
projectName: string,
|
|
9
|
-
packageName: string,
|
|
10
|
-
body: Record<string, unknown>,
|
|
11
|
-
) {
|
|
12
|
-
const options = this.validateCreateBody(body);
|
|
13
|
-
return this.materializationService.createMaterialization(
|
|
14
|
-
projectName,
|
|
15
|
-
packageName,
|
|
16
|
-
options,
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
private validateCreateBody(body: Record<string, unknown>): {
|
|
21
|
-
forceRefresh?: boolean;
|
|
22
|
-
autoLoadManifest?: boolean;
|
|
23
|
-
} {
|
|
24
|
-
const result: { forceRefresh?: boolean; autoLoadManifest?: boolean } = {};
|
|
25
|
-
if (body.forceRefresh !== undefined) {
|
|
26
|
-
if (typeof body.forceRefresh !== "boolean") {
|
|
27
|
-
throw new BadRequestError("forceRefresh must be a boolean");
|
|
28
|
-
}
|
|
29
|
-
result.forceRefresh = body.forceRefresh;
|
|
30
|
-
}
|
|
31
|
-
if (body.autoLoadManifest !== undefined) {
|
|
32
|
-
if (typeof body.autoLoadManifest !== "boolean") {
|
|
33
|
-
throw new BadRequestError("autoLoadManifest must be a boolean");
|
|
34
|
-
}
|
|
35
|
-
result.autoLoadManifest = body.autoLoadManifest;
|
|
36
|
-
}
|
|
37
|
-
return result;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async startMaterialization(
|
|
41
|
-
projectName: string,
|
|
42
|
-
packageName: string,
|
|
43
|
-
materializationId: string,
|
|
44
|
-
) {
|
|
45
|
-
return this.materializationService.startMaterialization(
|
|
46
|
-
projectName,
|
|
47
|
-
packageName,
|
|
48
|
-
materializationId,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async stopMaterialization(
|
|
53
|
-
projectName: string,
|
|
54
|
-
packageName: string,
|
|
55
|
-
materializationId: string,
|
|
56
|
-
) {
|
|
57
|
-
return this.materializationService.stopMaterialization(
|
|
58
|
-
projectName,
|
|
59
|
-
packageName,
|
|
60
|
-
materializationId,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async listMaterializations(
|
|
65
|
-
projectName: string,
|
|
66
|
-
packageName: string,
|
|
67
|
-
options?: { limit?: number; offset?: number },
|
|
68
|
-
) {
|
|
69
|
-
return this.materializationService.listMaterializations(
|
|
70
|
-
projectName,
|
|
71
|
-
packageName,
|
|
72
|
-
options,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async getMaterialization(
|
|
77
|
-
projectName: string,
|
|
78
|
-
packageName: string,
|
|
79
|
-
materializationId: string,
|
|
80
|
-
) {
|
|
81
|
-
return this.materializationService.getMaterialization(
|
|
82
|
-
projectName,
|
|
83
|
-
packageName,
|
|
84
|
-
materializationId,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async deleteMaterialization(
|
|
89
|
-
projectName: string,
|
|
90
|
-
packageName: string,
|
|
91
|
-
materializationId: string,
|
|
92
|
-
) {
|
|
93
|
-
return this.materializationService.deleteMaterialization(
|
|
94
|
-
projectName,
|
|
95
|
-
packageName,
|
|
96
|
-
materializationId,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async teardownPackage(
|
|
101
|
-
projectName: string,
|
|
102
|
-
packageName: string,
|
|
103
|
-
body: Record<string, unknown>,
|
|
104
|
-
) {
|
|
105
|
-
const options = this.validateTeardownBody(body);
|
|
106
|
-
return this.materializationService.teardownPackage(
|
|
107
|
-
projectName,
|
|
108
|
-
packageName,
|
|
109
|
-
options,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private validateTeardownBody(body: Record<string, unknown>): {
|
|
114
|
-
dryRun?: boolean;
|
|
115
|
-
} {
|
|
116
|
-
const options: { dryRun?: boolean } = {};
|
|
117
|
-
if (body.dryRun !== undefined) {
|
|
118
|
-
if (typeof body.dryRun !== "boolean") {
|
|
119
|
-
throw new BadRequestError("dryRun must be a boolean");
|
|
120
|
-
}
|
|
121
|
-
options.dryRun = body.dryRun;
|
|
122
|
-
}
|
|
123
|
-
return options;
|
|
124
|
-
}
|
|
125
|
-
}
|