@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
package/src/service/project.ts
DELETED
|
@@ -1,681 +0,0 @@
|
|
|
1
|
-
import type { LogMessage } from "@malloydata/malloy";
|
|
2
|
-
import { FixedConnectionMap, MalloyError, Runtime } from "@malloydata/malloy";
|
|
3
|
-
import { BaseConnection } from "@malloydata/malloy/connection";
|
|
4
|
-
import { Mutex } from "async-mutex";
|
|
5
|
-
import * as fs from "fs";
|
|
6
|
-
import * as path from "path";
|
|
7
|
-
import { components } from "../api";
|
|
8
|
-
import { API_PREFIX, README_NAME } from "../constants";
|
|
9
|
-
import {
|
|
10
|
-
ConnectionNotFoundError,
|
|
11
|
-
PackageNotFoundError,
|
|
12
|
-
ProjectNotFoundError,
|
|
13
|
-
} from "../errors";
|
|
14
|
-
import { logger } from "../logger";
|
|
15
|
-
import { URL_READER } from "../utils";
|
|
16
|
-
import {
|
|
17
|
-
createProjectConnections,
|
|
18
|
-
deleteDuckLakeConnectionFile,
|
|
19
|
-
InternalConnection,
|
|
20
|
-
} from "./connection";
|
|
21
|
-
import { ApiConnection } from "./model";
|
|
22
|
-
import { Package } from "./package";
|
|
23
|
-
|
|
24
|
-
export enum PackageStatus {
|
|
25
|
-
LOADING = "loading",
|
|
26
|
-
SERVING = "serving",
|
|
27
|
-
UNLOADING = "unloading",
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface PackageInfo {
|
|
31
|
-
name: string;
|
|
32
|
-
loadTimestamp: number;
|
|
33
|
-
status: PackageStatus;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type ApiPackage = components["schemas"]["Package"];
|
|
37
|
-
type ApiProject = components["schemas"]["Project"];
|
|
38
|
-
|
|
39
|
-
export class Project {
|
|
40
|
-
private packages: Map<string, Package> = new Map();
|
|
41
|
-
private packageMutexes = new Map<string, Mutex>();
|
|
42
|
-
private packageStatuses: Map<string, PackageInfo> = new Map();
|
|
43
|
-
private malloyConnections: Map<string, BaseConnection>;
|
|
44
|
-
private apiConnections: ApiConnection[];
|
|
45
|
-
private projectPath: string;
|
|
46
|
-
private projectName: string;
|
|
47
|
-
public metadata: ApiProject;
|
|
48
|
-
|
|
49
|
-
constructor(
|
|
50
|
-
projectName: string,
|
|
51
|
-
projectPath: string,
|
|
52
|
-
malloyConnections: Map<string, BaseConnection>,
|
|
53
|
-
apiConnections: InternalConnection[],
|
|
54
|
-
) {
|
|
55
|
-
this.projectName = projectName;
|
|
56
|
-
this.projectPath = projectPath;
|
|
57
|
-
this.malloyConnections = malloyConnections;
|
|
58
|
-
this.apiConnections = apiConnections;
|
|
59
|
-
this.metadata = {
|
|
60
|
-
resource: `${API_PREFIX}/projects/${this.projectName}`,
|
|
61
|
-
name: this.projectName,
|
|
62
|
-
location: this.projectPath,
|
|
63
|
-
};
|
|
64
|
-
void this.reloadProjectMetadata();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private async writeProjectReadme(readme?: string): Promise<void> {
|
|
68
|
-
if (readme === undefined) return;
|
|
69
|
-
|
|
70
|
-
const readmePath = path.join(this.projectPath, "README.md");
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
await fs.promises.writeFile(readmePath, readme, "utf-8");
|
|
74
|
-
logger.info(`Updated README.md for project ${this.projectName}`);
|
|
75
|
-
} catch (err) {
|
|
76
|
-
logger.error(`Failed to write README.md`, { error: err });
|
|
77
|
-
throw new Error(`Failed to update project README`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
public async update(payload: ApiProject) {
|
|
82
|
-
if (payload.readme !== undefined) {
|
|
83
|
-
this.metadata.readme = payload.readme;
|
|
84
|
-
await this.writeProjectReadme(payload.readme);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Handle connections update
|
|
88
|
-
// TODO: Update project connections should have its own API endpoint
|
|
89
|
-
if (payload.connections) {
|
|
90
|
-
logger.info(
|
|
91
|
-
`Updating ${payload.connections.length} connections for project ${this.projectName}`,
|
|
92
|
-
);
|
|
93
|
-
const isUpdateConnectionRequest = true;
|
|
94
|
-
// Reload connections with full config
|
|
95
|
-
const { malloyConnections, apiConnections } =
|
|
96
|
-
await createProjectConnections(
|
|
97
|
-
payload.connections,
|
|
98
|
-
this.projectPath,
|
|
99
|
-
isUpdateConnectionRequest,
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
// Update the project's connection maps
|
|
103
|
-
this.malloyConnections = malloyConnections;
|
|
104
|
-
this.apiConnections = apiConnections;
|
|
105
|
-
|
|
106
|
-
logger.info(
|
|
107
|
-
`Successfully updated connections for project ${this.projectName}`,
|
|
108
|
-
{
|
|
109
|
-
malloyConnections: malloyConnections.size,
|
|
110
|
-
apiConnections: apiConnections.length,
|
|
111
|
-
internalConnections: apiConnections.length,
|
|
112
|
-
},
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return this;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
static async create(
|
|
120
|
-
projectName: string,
|
|
121
|
-
projectPath: string,
|
|
122
|
-
connections: ApiConnection[],
|
|
123
|
-
): Promise<Project> {
|
|
124
|
-
if (!(await fs.promises.stat(projectPath))?.isDirectory()) {
|
|
125
|
-
throw new ProjectNotFoundError(
|
|
126
|
-
`Project path ${projectPath} not found`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
logger.info(`Creating project with connection configuration`);
|
|
131
|
-
const { malloyConnections, apiConnections } =
|
|
132
|
-
await createProjectConnections(connections, projectPath);
|
|
133
|
-
|
|
134
|
-
logger.info(
|
|
135
|
-
`Loaded ${malloyConnections.size + apiConnections.length} connections for project ${projectName}`,
|
|
136
|
-
{
|
|
137
|
-
malloyConnections,
|
|
138
|
-
apiConnections,
|
|
139
|
-
},
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
const project = new Project(
|
|
143
|
-
projectName,
|
|
144
|
-
projectPath,
|
|
145
|
-
malloyConnections,
|
|
146
|
-
apiConnections,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
return project;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
public async reloadProjectMetadata(): Promise<ApiProject> {
|
|
153
|
-
let readme = "";
|
|
154
|
-
try {
|
|
155
|
-
readme = (
|
|
156
|
-
await fs.promises.readFile(path.join(this.projectPath, README_NAME))
|
|
157
|
-
).toString();
|
|
158
|
-
} catch {
|
|
159
|
-
// Readme not found, so we'll just return an empty string
|
|
160
|
-
}
|
|
161
|
-
this.metadata = {
|
|
162
|
-
...this.metadata,
|
|
163
|
-
resource: `${API_PREFIX}/projects/${this.projectName}`,
|
|
164
|
-
name: this.projectName,
|
|
165
|
-
readme,
|
|
166
|
-
};
|
|
167
|
-
return this.metadata;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
public async compileSource(
|
|
171
|
-
packageName: string,
|
|
172
|
-
modelName: string,
|
|
173
|
-
source: string,
|
|
174
|
-
includeSql: boolean = false,
|
|
175
|
-
): Promise<{ problems: LogMessage[]; sql?: string }> {
|
|
176
|
-
// Place the virtual file in the model's directory so relative imports resolve correctly.
|
|
177
|
-
const modelDir = path.dirname(
|
|
178
|
-
path.join(this.projectPath, packageName, modelName),
|
|
179
|
-
);
|
|
180
|
-
const virtualUri = `file://${path.join(modelDir, "__compile_check.malloy")}`;
|
|
181
|
-
const virtualUrl = new URL(virtualUri);
|
|
182
|
-
|
|
183
|
-
// Read the full model file so the submitted source inherits the model's
|
|
184
|
-
// complete namespace — imports, source definitions, queries, etc.
|
|
185
|
-
const modelPath = path.join(this.projectPath, packageName, modelName);
|
|
186
|
-
let modelContent = "";
|
|
187
|
-
try {
|
|
188
|
-
modelContent = await fs.promises.readFile(modelPath, "utf8");
|
|
189
|
-
} catch {
|
|
190
|
-
// If the model file can't be read, proceed with empty content
|
|
191
|
-
// and let compilation surface any errors naturally.
|
|
192
|
-
}
|
|
193
|
-
const fullSource = modelContent ? `${modelContent}\n${source}` : source;
|
|
194
|
-
|
|
195
|
-
// Create a URL Reader that serves the source string for the virtual file,
|
|
196
|
-
// but falls back to the disk for everything else (imports).
|
|
197
|
-
const interceptingReader = {
|
|
198
|
-
readURL: async (url: URL) => {
|
|
199
|
-
if (url.toString() === virtualUri) {
|
|
200
|
-
return fullSource;
|
|
201
|
-
}
|
|
202
|
-
return URL_READER.readURL(url);
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// Initialize Runtime with the project's active connections
|
|
207
|
-
const runtime = new Runtime({
|
|
208
|
-
urlReader: interceptingReader,
|
|
209
|
-
connections: new FixedConnectionMap(this.malloyConnections, "duckdb"),
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// Attempt to compile
|
|
213
|
-
try {
|
|
214
|
-
const modelMaterializer = runtime.loadModel(virtualUrl);
|
|
215
|
-
const model = await modelMaterializer.getModel();
|
|
216
|
-
|
|
217
|
-
// If includeSql is requested and compilation succeeded, attempt to extract SQL
|
|
218
|
-
let sql: string | undefined;
|
|
219
|
-
if (includeSql) {
|
|
220
|
-
try {
|
|
221
|
-
const queryMaterializer = modelMaterializer.loadFinalQuery();
|
|
222
|
-
sql = await queryMaterializer.getSQL();
|
|
223
|
-
} catch {
|
|
224
|
-
// Source may not contain a runnable query (e.g. only source definitions),
|
|
225
|
-
// in which case we simply omit the sql field.
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// If successful, return any non-fatal warnings
|
|
230
|
-
return { problems: model.problems, sql };
|
|
231
|
-
} catch (error) {
|
|
232
|
-
// If parsing/compilation fails, return the errors
|
|
233
|
-
if (error instanceof MalloyError) {
|
|
234
|
-
return { problems: error.problems };
|
|
235
|
-
}
|
|
236
|
-
// If it's a system error (e.g. file not found), throw it up
|
|
237
|
-
throw error;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
public listApiConnections(): ApiConnection[] {
|
|
242
|
-
return this.apiConnections;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
public getApiConnection(connectionName: string): ApiConnection {
|
|
246
|
-
const connection = this.apiConnections.find(
|
|
247
|
-
(connection) => connection.name === connectionName,
|
|
248
|
-
);
|
|
249
|
-
if (!connection) {
|
|
250
|
-
throw new ConnectionNotFoundError(
|
|
251
|
-
`Connection ${connectionName} not found`,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
return connection;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
public getMalloyConnection(connectionName: string): BaseConnection {
|
|
258
|
-
const connection = this.malloyConnections.get(connectionName);
|
|
259
|
-
if (!connection) {
|
|
260
|
-
throw new ConnectionNotFoundError(
|
|
261
|
-
`Connection ${connectionName} not found`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
return connection;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
public async listPackages(): Promise<ApiPackage[]> {
|
|
268
|
-
logger.debug("Listing packages", { projectPath: this.projectPath });
|
|
269
|
-
try {
|
|
270
|
-
const packageMetadata = await Promise.all(
|
|
271
|
-
Array.from(this.packageStatuses.keys()).map(async (packageName) => {
|
|
272
|
-
try {
|
|
273
|
-
const packageMetadata = (
|
|
274
|
-
this.packageStatuses.get(packageName)?.status ===
|
|
275
|
-
PackageStatus.LOADING
|
|
276
|
-
? undefined
|
|
277
|
-
: await this.getPackage(packageName, false)
|
|
278
|
-
)?.getPackageMetadata();
|
|
279
|
-
if (packageMetadata) {
|
|
280
|
-
packageMetadata.name = packageName;
|
|
281
|
-
}
|
|
282
|
-
return packageMetadata;
|
|
283
|
-
} catch (error) {
|
|
284
|
-
logger.error(
|
|
285
|
-
`Failed to load package: ${packageName} due to : ${error}`,
|
|
286
|
-
);
|
|
287
|
-
// Directory did not contain a valid package.json file -- therefore, it's not a package.
|
|
288
|
-
// Or it timed out
|
|
289
|
-
return undefined;
|
|
290
|
-
}
|
|
291
|
-
}),
|
|
292
|
-
);
|
|
293
|
-
// Get rid of undefined entries (i.e, directories without publisher.json files).
|
|
294
|
-
const filteredMetadata = packageMetadata.filter(
|
|
295
|
-
(metadata) => metadata,
|
|
296
|
-
) as ApiPackage[];
|
|
297
|
-
|
|
298
|
-
// Filter out packages that are being unloaded
|
|
299
|
-
const finalMetadata = filteredMetadata.filter((metadata) => {
|
|
300
|
-
const packageStatus = this.packageStatuses.get(metadata.name || "");
|
|
301
|
-
return packageStatus?.status !== PackageStatus.UNLOADING;
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
return finalMetadata;
|
|
305
|
-
} catch (error) {
|
|
306
|
-
logger.error("Error listing packages", { error });
|
|
307
|
-
console.error(error);
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
public async getPackage(
|
|
313
|
-
packageName: string,
|
|
314
|
-
reload: boolean = false,
|
|
315
|
-
): Promise<Package> {
|
|
316
|
-
// Check if package is already loaded first
|
|
317
|
-
const _package = this.packages.get(packageName);
|
|
318
|
-
if (_package !== undefined && !reload) {
|
|
319
|
-
return _package;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// We need to acquire the mutex to prevent a thundering herd of requests from creating the
|
|
323
|
-
// package multiple times.
|
|
324
|
-
let packageMutex = this.packageMutexes.get(packageName);
|
|
325
|
-
if (packageMutex?.isLocked()) {
|
|
326
|
-
logger.debug(
|
|
327
|
-
`Package ${packageName} is being loaded, waiting for unlock...`,
|
|
328
|
-
);
|
|
329
|
-
await packageMutex.waitForUnlock();
|
|
330
|
-
logger.debug(`Package ${packageName} unlocked`);
|
|
331
|
-
const existingPackage = this.packages.get(packageName);
|
|
332
|
-
if (existingPackage) {
|
|
333
|
-
logger.debug(`Package ${packageName} loaded by another request`);
|
|
334
|
-
return existingPackage;
|
|
335
|
-
}
|
|
336
|
-
// If package still doesn't exist after unlock, it might have failed to load
|
|
337
|
-
// Continue to try loading it ourselves
|
|
338
|
-
}
|
|
339
|
-
packageMutex = new Mutex();
|
|
340
|
-
this.packageMutexes.set(packageName, packageMutex);
|
|
341
|
-
|
|
342
|
-
return packageMutex.runExclusive(async () => {
|
|
343
|
-
// Double-check after acquiring mutex
|
|
344
|
-
const existingPackage = this.packages.get(packageName);
|
|
345
|
-
if (existingPackage !== undefined && !reload) {
|
|
346
|
-
return existingPackage;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Set package status to loading
|
|
350
|
-
this.setPackageStatus(packageName, PackageStatus.LOADING);
|
|
351
|
-
|
|
352
|
-
try {
|
|
353
|
-
logger.debug(`Loading package ${packageName}...`);
|
|
354
|
-
const packagePath = path.join(this.projectPath, packageName);
|
|
355
|
-
const _package = await Package.create(
|
|
356
|
-
this.projectName,
|
|
357
|
-
packageName,
|
|
358
|
-
packagePath,
|
|
359
|
-
this.malloyConnections,
|
|
360
|
-
);
|
|
361
|
-
this.packages.set(packageName, _package);
|
|
362
|
-
|
|
363
|
-
// Set package status to serving
|
|
364
|
-
this.setPackageStatus(packageName, PackageStatus.SERVING);
|
|
365
|
-
logger.debug(`Successfully loaded package ${packageName}`);
|
|
366
|
-
|
|
367
|
-
return _package;
|
|
368
|
-
} catch (error) {
|
|
369
|
-
logger.error(`Failed to load package ${packageName}`, { error });
|
|
370
|
-
// Clean up on error - mutex will be automatically released by runExclusive
|
|
371
|
-
this.packages.delete(packageName);
|
|
372
|
-
this.packageStatuses.delete(packageName);
|
|
373
|
-
throw error;
|
|
374
|
-
}
|
|
375
|
-
// Mutex is automatically released here by runExclusive
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
public async addPackage(packageName: string) {
|
|
380
|
-
const packagePath = path.join(this.projectPath, packageName);
|
|
381
|
-
if (
|
|
382
|
-
!(await fs.promises
|
|
383
|
-
.access(packagePath)
|
|
384
|
-
.then(() => true)
|
|
385
|
-
.catch(() => false)) ||
|
|
386
|
-
!(await fs.promises.stat(packagePath))?.isDirectory()
|
|
387
|
-
) {
|
|
388
|
-
throw new PackageNotFoundError(`Package ${packageName} not found`);
|
|
389
|
-
}
|
|
390
|
-
logger.info(
|
|
391
|
-
`Adding package ${packageName} to project ${this.projectName}`,
|
|
392
|
-
{
|
|
393
|
-
packagePath,
|
|
394
|
-
malloyConnections: this.malloyConnections,
|
|
395
|
-
},
|
|
396
|
-
);
|
|
397
|
-
this.setPackageStatus(packageName, PackageStatus.LOADING);
|
|
398
|
-
try {
|
|
399
|
-
this.packages.set(
|
|
400
|
-
packageName,
|
|
401
|
-
await Package.create(
|
|
402
|
-
this.projectName,
|
|
403
|
-
packageName,
|
|
404
|
-
packagePath,
|
|
405
|
-
this.malloyConnections,
|
|
406
|
-
),
|
|
407
|
-
);
|
|
408
|
-
} catch (error) {
|
|
409
|
-
logger.error("Error adding package", { error });
|
|
410
|
-
this.deletePackageStatus(packageName);
|
|
411
|
-
throw error;
|
|
412
|
-
}
|
|
413
|
-
this.setPackageStatus(packageName, PackageStatus.SERVING);
|
|
414
|
-
return this.packages.get(packageName);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
private async writePackageManifest(
|
|
418
|
-
packageName: string,
|
|
419
|
-
metadata: { name: string; description?: string },
|
|
420
|
-
): Promise<void> {
|
|
421
|
-
const packagePath = path.join(this.projectPath, packageName);
|
|
422
|
-
const manifestPath = path.join(packagePath, "publisher.json");
|
|
423
|
-
|
|
424
|
-
try {
|
|
425
|
-
// Read existing manifest
|
|
426
|
-
let existingManifest: Record<string, unknown> = {};
|
|
427
|
-
try {
|
|
428
|
-
const content = await fs.promises.readFile(manifestPath, "utf-8");
|
|
429
|
-
existingManifest = JSON.parse(content);
|
|
430
|
-
} catch (_err) {
|
|
431
|
-
logger.warn(`Could not read manifest for ${packageName}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Update with new metadata
|
|
435
|
-
const updatedManifest = {
|
|
436
|
-
...existingManifest,
|
|
437
|
-
name: metadata.name,
|
|
438
|
-
description: metadata.description,
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
// Write back to file
|
|
442
|
-
await fs.promises.writeFile(
|
|
443
|
-
manifestPath,
|
|
444
|
-
JSON.stringify(updatedManifest, null, 2),
|
|
445
|
-
"utf-8",
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
logger.info(`Updated publisher.json for ${packageName}`);
|
|
449
|
-
} catch (error) {
|
|
450
|
-
logger.error(`Failed to update publisher.json`, { error });
|
|
451
|
-
throw new Error(`Failed to update package manifest`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
public async updatePackage(packageName: string, body: ApiPackage) {
|
|
456
|
-
const _package = this.packages.get(packageName);
|
|
457
|
-
if (!_package) {
|
|
458
|
-
throw new PackageNotFoundError(`Package ${packageName} not found`);
|
|
459
|
-
}
|
|
460
|
-
if (body.name) {
|
|
461
|
-
_package.setName(body.name);
|
|
462
|
-
}
|
|
463
|
-
_package.setPackageMetadata({
|
|
464
|
-
name: body.name,
|
|
465
|
-
description: body.description,
|
|
466
|
-
resource: body.resource,
|
|
467
|
-
location: body.location,
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
await this.writePackageManifest(packageName, {
|
|
471
|
-
name: packageName,
|
|
472
|
-
description: body.description,
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
return _package.getPackageMetadata();
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
public getPackageStatus(packageName: string): PackageInfo | undefined {
|
|
479
|
-
return this.packageStatuses.get(packageName);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
public setPackageStatus(packageName: string, status: PackageStatus): void {
|
|
483
|
-
const currentStatus = this.packageStatuses.get(packageName);
|
|
484
|
-
this.packageStatuses.set(packageName, {
|
|
485
|
-
name: packageName,
|
|
486
|
-
loadTimestamp: currentStatus?.loadTimestamp || Date.now(),
|
|
487
|
-
status: status,
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
public deletePackageStatus(packageName: string): void {
|
|
492
|
-
this.packageStatuses.delete(packageName);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
public async deletePackage(packageName: string): Promise<void> {
|
|
496
|
-
const _package = this.packages.get(packageName);
|
|
497
|
-
if (!_package) {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
const packageStatus = this.packageStatuses.get(packageName);
|
|
501
|
-
|
|
502
|
-
if (packageStatus?.status === PackageStatus.LOADING) {
|
|
503
|
-
logger.error("Package loading. Can't unload.", {
|
|
504
|
-
projectName: this.projectName,
|
|
505
|
-
packageName,
|
|
506
|
-
});
|
|
507
|
-
throw new Error(
|
|
508
|
-
"Package loading. Can't unload. " +
|
|
509
|
-
this.projectName +
|
|
510
|
-
" " +
|
|
511
|
-
packageName,
|
|
512
|
-
);
|
|
513
|
-
} else if (packageStatus?.status === PackageStatus.SERVING) {
|
|
514
|
-
this.setPackageStatus(packageName, PackageStatus.UNLOADING);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
try {
|
|
518
|
-
await fs.promises.rm(path.join(this.projectPath, packageName), {
|
|
519
|
-
recursive: true,
|
|
520
|
-
force: true,
|
|
521
|
-
});
|
|
522
|
-
} catch (err) {
|
|
523
|
-
logger.error(
|
|
524
|
-
"Error removing package directory while unloading package",
|
|
525
|
-
{ error: err, projectName: this.projectName, packageName },
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Remove from internal tracking
|
|
530
|
-
this.packages.delete(packageName);
|
|
531
|
-
this.packageStatuses.delete(packageName);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
public updateConnections(
|
|
535
|
-
malloyConnections: Map<string, BaseConnection>,
|
|
536
|
-
apiConnections: ApiConnection[],
|
|
537
|
-
): void {
|
|
538
|
-
this.malloyConnections = malloyConnections;
|
|
539
|
-
this.apiConnections = apiConnections;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
public async deleteConnection(connectionName: string): Promise<void> {
|
|
543
|
-
this.malloyConnections.get(connectionName)?.close();
|
|
544
|
-
const isDeleted = this.malloyConnections.delete(connectionName);
|
|
545
|
-
|
|
546
|
-
const index = this.apiConnections.findIndex(
|
|
547
|
-
(conn) => conn.name === connectionName,
|
|
548
|
-
);
|
|
549
|
-
|
|
550
|
-
const connectionType = this.apiConnections[index]?.type;
|
|
551
|
-
if (connectionType === "duckdb") {
|
|
552
|
-
await this.deleteDuckDBConnection(connectionName);
|
|
553
|
-
} else if (connectionType === "ducklake") {
|
|
554
|
-
await this.deleteDuckLakeConnection(connectionName);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if (index !== -1) {
|
|
558
|
-
this.apiConnections.splice(index, 1);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (isDeleted || index !== -1) {
|
|
562
|
-
logger.info(
|
|
563
|
-
`Removed connection ${connectionName} from project ${this.projectName}`,
|
|
564
|
-
);
|
|
565
|
-
} else {
|
|
566
|
-
logger.warn(
|
|
567
|
-
`Connection ${connectionName} not found in project ${this.projectName}`,
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
public closeAllConnections(): void {
|
|
573
|
-
// Close all Malloy connections
|
|
574
|
-
for (const [connectionName, connection] of this.malloyConnections) {
|
|
575
|
-
try {
|
|
576
|
-
connection.close();
|
|
577
|
-
logger.info(
|
|
578
|
-
`Closed connection ${connectionName} for project ${this.projectName}`,
|
|
579
|
-
);
|
|
580
|
-
} catch (error) {
|
|
581
|
-
logger.error(
|
|
582
|
-
`Error closing connection ${connectionName} for project ${this.projectName}`,
|
|
583
|
-
{ error },
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Clear connection maps
|
|
589
|
-
this.malloyConnections.clear();
|
|
590
|
-
this.apiConnections = [];
|
|
591
|
-
|
|
592
|
-
logger.info(`Closed all connections for project ${this.projectName}`);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
public async serialize(): Promise<ApiProject> {
|
|
596
|
-
return {
|
|
597
|
-
...this.metadata,
|
|
598
|
-
connections: this.listApiConnections(),
|
|
599
|
-
packages: await this.listPackages(),
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
public async deleteDuckDBConnection(connectionName: string): Promise<void> {
|
|
604
|
-
const duckdbPath = path.join(
|
|
605
|
-
this.projectPath,
|
|
606
|
-
`${connectionName}.duckdb`,
|
|
607
|
-
);
|
|
608
|
-
fs.promises
|
|
609
|
-
.access(duckdbPath)
|
|
610
|
-
.then(() => {
|
|
611
|
-
fs.promises
|
|
612
|
-
.rm(duckdbPath)
|
|
613
|
-
.then(() => {
|
|
614
|
-
logger.info(
|
|
615
|
-
`Removed DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
616
|
-
);
|
|
617
|
-
})
|
|
618
|
-
.catch((error) => {
|
|
619
|
-
logger.error(
|
|
620
|
-
`Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
621
|
-
{ error },
|
|
622
|
-
);
|
|
623
|
-
});
|
|
624
|
-
})
|
|
625
|
-
.catch((error) => {
|
|
626
|
-
logger.error(
|
|
627
|
-
`Failed to remove DuckDB connection file ${connectionName} from project ${this.projectName}`,
|
|
628
|
-
{ error },
|
|
629
|
-
);
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
public async deleteDuckLakeConnection(
|
|
634
|
-
connectionName: string,
|
|
635
|
-
): Promise<void> {
|
|
636
|
-
await deleteDuckLakeConnectionFile(connectionName, this.projectPath);
|
|
637
|
-
logger.info(
|
|
638
|
-
`Removed DuckLake connection ${connectionName} from project ${this.projectName}`,
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Extracts the preamble from a Malloy model file — the leading block of
|
|
645
|
-
* `##!` pragmas, `import` statements, blank lines, and comments that appear
|
|
646
|
-
* before any `source:`, `query:`, or `run:` definition. This allows a
|
|
647
|
-
* submitted query to inherit the model's import context.
|
|
648
|
-
*/
|
|
649
|
-
export async function extractPreamble(modelPath: string): Promise<string> {
|
|
650
|
-
try {
|
|
651
|
-
const content = await fs.promises.readFile(modelPath, "utf8");
|
|
652
|
-
return extractPreambleFromSource(content);
|
|
653
|
-
} catch {
|
|
654
|
-
// If the model file can't be read, return empty preamble
|
|
655
|
-
// and let the compilation surface any import errors naturally.
|
|
656
|
-
return "";
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Extracts the preamble from Malloy source text. Exported for testing.
|
|
662
|
-
*/
|
|
663
|
-
export function extractPreambleFromSource(content: string): string {
|
|
664
|
-
const lines = content.split("\n");
|
|
665
|
-
const preambleLines: string[] = [];
|
|
666
|
-
|
|
667
|
-
for (const line of lines) {
|
|
668
|
-
const trimmed = line.trim();
|
|
669
|
-
// Stop at the first source/query/run definition
|
|
670
|
-
if (
|
|
671
|
-
trimmed.startsWith("source:") ||
|
|
672
|
-
trimmed.startsWith("query:") ||
|
|
673
|
-
trimmed.startsWith("run:")
|
|
674
|
-
) {
|
|
675
|
-
break;
|
|
676
|
-
}
|
|
677
|
-
preambleLines.push(line);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
return preambleLines.join("\n").trimEnd();
|
|
681
|
-
}
|