@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,236 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { Stats } from "fs";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import sinon from "sinon";
|
|
6
|
-
import { PackageNotFoundError } from "../errors";
|
|
7
|
-
import { Model } from "./model";
|
|
8
|
-
import { Package } from "./package";
|
|
9
|
-
|
|
10
|
-
// Minimal partial types for mocking
|
|
11
|
-
type PartialModel = Pick<Model, "getPath">;
|
|
12
|
-
|
|
13
|
-
describe("service/package", () => {
|
|
14
|
-
const testPackageDirectory = "testPackage";
|
|
15
|
-
|
|
16
|
-
beforeEach(async () => {
|
|
17
|
-
await fs.mkdir(testPackageDirectory, { recursive: true });
|
|
18
|
-
await fs.writeFile(join(testPackageDirectory, "model1.model"), "");
|
|
19
|
-
// Create a simple parquet file with schema "name: string, value: int"
|
|
20
|
-
const parquetBuffer = Buffer.from("Name,Value\nJohn,10\nJane,20\nJim,30");
|
|
21
|
-
|
|
22
|
-
await fs.writeFile(
|
|
23
|
-
join(testPackageDirectory, "database.csv"),
|
|
24
|
-
parquetBuffer,
|
|
25
|
-
);
|
|
26
|
-
const publisherContent = JSON.stringify({ description: "Test package" });
|
|
27
|
-
await fs.writeFile(
|
|
28
|
-
join(testPackageDirectory, "publisher.json"),
|
|
29
|
-
publisherContent,
|
|
30
|
-
);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
afterEach(async () => {
|
|
34
|
-
sinon.restore();
|
|
35
|
-
// On Windows, DuckDB connections may still have file handles open,
|
|
36
|
-
// causing EBUSY errors. Retry deletion with exponential backoff.
|
|
37
|
-
const maxRetries = 3;
|
|
38
|
-
const delay = 50; // Start with 50ms
|
|
39
|
-
let lastError: unknown;
|
|
40
|
-
|
|
41
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
42
|
-
try {
|
|
43
|
-
await fs.rm(testPackageDirectory, { recursive: true, force: true });
|
|
44
|
-
return; // Success, exit retry loop
|
|
45
|
-
} catch (error) {
|
|
46
|
-
lastError = error;
|
|
47
|
-
const errnoError = error as NodeJS.ErrnoException;
|
|
48
|
-
// Only retry on EBUSY errors (Windows file handle still open)
|
|
49
|
-
if (errnoError.code !== "EBUSY") {
|
|
50
|
-
throw error; // Non-EBUSY errors should be thrown immediately
|
|
51
|
-
}
|
|
52
|
-
// Exponential backoff: 50ms, 100ms, 200ms, 400ms, 800ms
|
|
53
|
-
if (attempt < maxRetries - 1) {
|
|
54
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// If we've exhausted retries, ignore EBUSY errors as they don't affect test results
|
|
59
|
-
// and the files will be cleaned up eventually
|
|
60
|
-
if ((lastError as NodeJS.ErrnoException).code !== "EBUSY") {
|
|
61
|
-
throw lastError;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("should create a package instance", async () => {
|
|
66
|
-
// Using 'as any' for simplified mock Map value in test
|
|
67
|
-
const pkg = new Package(
|
|
68
|
-
"testProject",
|
|
69
|
-
"testPackage",
|
|
70
|
-
testPackageDirectory,
|
|
71
|
-
{ name: "testPackage", description: "Test package" },
|
|
72
|
-
[],
|
|
73
|
-
new Map([
|
|
74
|
-
[
|
|
75
|
-
"model1.malloy",
|
|
76
|
-
{ getPath: () => "model1.malloy" } as unknown as Model,
|
|
77
|
-
],
|
|
78
|
-
[
|
|
79
|
-
"model2.malloynb",
|
|
80
|
-
{ getPath: () => "model2.malloynb" } as unknown as Model,
|
|
81
|
-
],
|
|
82
|
-
]),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
expect(pkg).toBeInstanceOf(Package);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe("instance methods", () => {
|
|
89
|
-
describe("create", () => {
|
|
90
|
-
it("should throw PackageNotFoundError if the package manifest does not exist", async () => {
|
|
91
|
-
await fs.rm(join(testPackageDirectory, "publisher.json"));
|
|
92
|
-
sinon.stub(fs, "stat").rejects(new Error("File not found"));
|
|
93
|
-
|
|
94
|
-
await expect(
|
|
95
|
-
Package.create(
|
|
96
|
-
"testProject",
|
|
97
|
-
"testPackage",
|
|
98
|
-
testPackageDirectory,
|
|
99
|
-
new Map(),
|
|
100
|
-
),
|
|
101
|
-
).rejects.toThrowError(
|
|
102
|
-
new PackageNotFoundError(
|
|
103
|
-
"Package manifest for testPackage does not exist.",
|
|
104
|
-
),
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
it(
|
|
108
|
-
"should return a Package object if the package exists",
|
|
109
|
-
async () => {
|
|
110
|
-
sinon.stub(fs, "stat").resolves();
|
|
111
|
-
const readFileStub = sinon
|
|
112
|
-
.stub(fs, "readFile")
|
|
113
|
-
.resolves(
|
|
114
|
-
Buffer.from(
|
|
115
|
-
JSON.stringify({ description: "Test package" }),
|
|
116
|
-
),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
// Still use Partial<Model> for the stub resolution type
|
|
120
|
-
sinon
|
|
121
|
-
.stub(Model, "create")
|
|
122
|
-
// @ts-expect-error PartialModel is a partial type
|
|
123
|
-
.resolves({ getPath: () => "model1.model" } as PartialModel);
|
|
124
|
-
|
|
125
|
-
readFileStub.restore();
|
|
126
|
-
readFileStub.resolves(Buffer.from(JSON.stringify([])));
|
|
127
|
-
|
|
128
|
-
const packageInstance = await Package.create(
|
|
129
|
-
"testProject",
|
|
130
|
-
"testPackage",
|
|
131
|
-
testPackageDirectory,
|
|
132
|
-
new Map(),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
expect(packageInstance).toBeInstanceOf(Package);
|
|
136
|
-
expect(packageInstance.getPackageName()).toBe("testPackage");
|
|
137
|
-
expect(packageInstance.getPackageMetadata().description).toBe(
|
|
138
|
-
"Test package",
|
|
139
|
-
);
|
|
140
|
-
expect(packageInstance.listDatabases()).toEqual([
|
|
141
|
-
{
|
|
142
|
-
path: "database.csv",
|
|
143
|
-
type: "embedded",
|
|
144
|
-
info: {
|
|
145
|
-
name: "database.csv",
|
|
146
|
-
columns: [
|
|
147
|
-
{ name: "Name", type: "string" },
|
|
148
|
-
{ name: "Value", type: "number" },
|
|
149
|
-
],
|
|
150
|
-
rowCount: 3,
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
]);
|
|
154
|
-
expect(packageInstance.listModels()).toBeEmpty();
|
|
155
|
-
},
|
|
156
|
-
{ timeout: 20000 },
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe("listModels", () => {
|
|
161
|
-
it("should return a list of models with their paths and types", async () => {
|
|
162
|
-
// Using 'as any' for simplified mock Map value in test
|
|
163
|
-
const packageInstance = new Package(
|
|
164
|
-
"testProject",
|
|
165
|
-
"testPackage",
|
|
166
|
-
testPackageDirectory,
|
|
167
|
-
{ name: "testPackage", description: "Test package" },
|
|
168
|
-
[],
|
|
169
|
-
new Map([
|
|
170
|
-
[
|
|
171
|
-
"model1.malloy",
|
|
172
|
-
{
|
|
173
|
-
getPath: () => "model1.malloy",
|
|
174
|
-
getModel: () => "foo",
|
|
175
|
-
} as unknown as Model,
|
|
176
|
-
],
|
|
177
|
-
[
|
|
178
|
-
"model2.malloynb",
|
|
179
|
-
{
|
|
180
|
-
getPath: () => "model2.malloynb",
|
|
181
|
-
getNotebookError: () => {
|
|
182
|
-
return {
|
|
183
|
-
message: "This is the error",
|
|
184
|
-
};
|
|
185
|
-
},
|
|
186
|
-
} as unknown as Model,
|
|
187
|
-
],
|
|
188
|
-
]),
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
const models = await packageInstance.listModels();
|
|
192
|
-
expect(models).toEqual([
|
|
193
|
-
{
|
|
194
|
-
// @ts-expect-error TODO: Fix missing projectName type in API
|
|
195
|
-
projectName: "testProject",
|
|
196
|
-
packageName: "testPackage",
|
|
197
|
-
path: "model1.malloy",
|
|
198
|
-
error: undefined,
|
|
199
|
-
},
|
|
200
|
-
]);
|
|
201
|
-
|
|
202
|
-
const notebooks = await packageInstance.listNotebooks();
|
|
203
|
-
expect(notebooks).toEqual([
|
|
204
|
-
{
|
|
205
|
-
// @ts-expect-error TODO: Fix missing projectName type in API
|
|
206
|
-
projectName: "testProject",
|
|
207
|
-
packageName: "testPackage",
|
|
208
|
-
path: "model2.malloynb",
|
|
209
|
-
error: "This is the error",
|
|
210
|
-
},
|
|
211
|
-
]);
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
describe("getDatabaseInfo", () => {
|
|
216
|
-
it("should return the size of the database file", async () => {
|
|
217
|
-
sinon.stub(fs, "stat").resolves({ size: 13 } as Stats);
|
|
218
|
-
|
|
219
|
-
// @ts-expect-error Accessing private static method for testing
|
|
220
|
-
const info = await Package.getDatabaseInfo(
|
|
221
|
-
testPackageDirectory,
|
|
222
|
-
"database.csv",
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
expect(info).toEqual({
|
|
226
|
-
name: "database.csv",
|
|
227
|
-
columns: [
|
|
228
|
-
{ name: "Name", type: "string" },
|
|
229
|
-
{ name: "Value", type: "number" },
|
|
230
|
-
],
|
|
231
|
-
rowCount: 3,
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|
package/src/service/package.ts
DELETED
|
@@ -1,463 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs/promises";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
|
|
4
|
-
import { DuckDBConnection } from "@malloydata/db-duckdb";
|
|
5
|
-
import {
|
|
6
|
-
Connection,
|
|
7
|
-
ConnectionRuntime,
|
|
8
|
-
EmptyURLReader,
|
|
9
|
-
SourceDef,
|
|
10
|
-
} from "@malloydata/malloy";
|
|
11
|
-
import { metrics } from "@opentelemetry/api";
|
|
12
|
-
import recursive from "recursive-readdir";
|
|
13
|
-
import { components } from "../api";
|
|
14
|
-
import {
|
|
15
|
-
API_PREFIX,
|
|
16
|
-
MODEL_FILE_SUFFIX,
|
|
17
|
-
NOTEBOOK_FILE_SUFFIX,
|
|
18
|
-
PACKAGE_MANIFEST_NAME,
|
|
19
|
-
} from "../constants";
|
|
20
|
-
import { PackageNotFoundError } from "../errors";
|
|
21
|
-
import { formatDuration, logger } from "../logger";
|
|
22
|
-
import { BuildManifest } from "../storage/DatabaseInterface";
|
|
23
|
-
import { Model } from "./model";
|
|
24
|
-
|
|
25
|
-
type ApiDatabase = components["schemas"]["Database"];
|
|
26
|
-
type ApiModel = components["schemas"]["Model"];
|
|
27
|
-
type ApiNotebook = components["schemas"]["Notebook"];
|
|
28
|
-
export type ApiPackage = components["schemas"]["Package"];
|
|
29
|
-
type ApiColumn = components["schemas"]["Column"];
|
|
30
|
-
type ApiTableDescription = components["schemas"]["TableDescription"];
|
|
31
|
-
|
|
32
|
-
const ENABLE_LIST_MODEL_COMPILATION = true;
|
|
33
|
-
export class Package {
|
|
34
|
-
private projectName: string;
|
|
35
|
-
private packageName: string;
|
|
36
|
-
private packageMetadata: ApiPackage;
|
|
37
|
-
private databases: ApiDatabase[];
|
|
38
|
-
private models: Map<string, Model> = new Map();
|
|
39
|
-
private packagePath: string;
|
|
40
|
-
private connections: Map<string, Connection> = new Map();
|
|
41
|
-
private static meter = metrics.getMeter("publisher");
|
|
42
|
-
private static packageLoadHistogram = this.meter.createHistogram(
|
|
43
|
-
"malloy_package_load_duration",
|
|
44
|
-
{
|
|
45
|
-
description: "Time taken to load a Malloy package",
|
|
46
|
-
unit: "ms",
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
constructor(
|
|
51
|
-
projectName: string,
|
|
52
|
-
packageName: string,
|
|
53
|
-
packagePath: string,
|
|
54
|
-
packageMetadata: ApiPackage,
|
|
55
|
-
databases: ApiDatabase[],
|
|
56
|
-
models: Map<string, Model>,
|
|
57
|
-
connections: Map<string, Connection> = new Map(),
|
|
58
|
-
) {
|
|
59
|
-
this.projectName = projectName;
|
|
60
|
-
this.packageName = packageName;
|
|
61
|
-
this.packagePath = packagePath;
|
|
62
|
-
this.packageMetadata = packageMetadata;
|
|
63
|
-
this.databases = databases;
|
|
64
|
-
this.models = models;
|
|
65
|
-
this.connections = connections;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
static async create(
|
|
69
|
-
projectName: string,
|
|
70
|
-
packageName: string,
|
|
71
|
-
packagePath: string,
|
|
72
|
-
projectConnections: Map<string, Connection>,
|
|
73
|
-
): Promise<Package> {
|
|
74
|
-
const startTime = performance.now();
|
|
75
|
-
await Package.validatePackageManifestExistsOrThrowError(packagePath);
|
|
76
|
-
const manifestValidationTime = performance.now();
|
|
77
|
-
logger.info("Package manifest validation completed", {
|
|
78
|
-
packageName,
|
|
79
|
-
duration: formatDuration(manifestValidationTime - startTime),
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const packageConfig = await Package.readPackageConfig(packagePath);
|
|
84
|
-
const packageConfigTime = performance.now();
|
|
85
|
-
logger.info("Package config read completed", {
|
|
86
|
-
packageName,
|
|
87
|
-
duration: formatDuration(
|
|
88
|
-
packageConfigTime - manifestValidationTime,
|
|
89
|
-
),
|
|
90
|
-
});
|
|
91
|
-
packageConfig.resource = `${API_PREFIX}/projects/${projectName}/packages/${packageName}`;
|
|
92
|
-
|
|
93
|
-
const databases = await Package.readDatabases(packagePath);
|
|
94
|
-
const databasesTime = performance.now();
|
|
95
|
-
logger.info("Databases read completed", {
|
|
96
|
-
packageName,
|
|
97
|
-
databaseCount: databases.length,
|
|
98
|
-
duration: formatDuration(databasesTime - packageConfigTime),
|
|
99
|
-
});
|
|
100
|
-
const connections = new Map<string, Connection>(projectConnections);
|
|
101
|
-
|
|
102
|
-
// Add a duckdb connection for the package.
|
|
103
|
-
const duckdbConnection = new DuckDBConnection(
|
|
104
|
-
"duckdb",
|
|
105
|
-
":memory:",
|
|
106
|
-
packagePath,
|
|
107
|
-
);
|
|
108
|
-
connections.set("duckdb", duckdbConnection);
|
|
109
|
-
|
|
110
|
-
const models = await Package.loadModels(
|
|
111
|
-
packageName,
|
|
112
|
-
packagePath,
|
|
113
|
-
connections,
|
|
114
|
-
);
|
|
115
|
-
const modelsTime = performance.now();
|
|
116
|
-
logger.info("Models loaded", {
|
|
117
|
-
packageName,
|
|
118
|
-
modelCount: models.size,
|
|
119
|
-
duration: formatDuration(modelsTime - databasesTime),
|
|
120
|
-
});
|
|
121
|
-
for (const [modelPath, model] of models.entries()) {
|
|
122
|
-
const maybeModel = model as unknown as {
|
|
123
|
-
compilationError?: unknown;
|
|
124
|
-
};
|
|
125
|
-
if (maybeModel.compilationError) {
|
|
126
|
-
const err = maybeModel.compilationError;
|
|
127
|
-
const message =
|
|
128
|
-
err instanceof Error
|
|
129
|
-
? err.message
|
|
130
|
-
: `Unknown compilation error in ${modelPath}`;
|
|
131
|
-
|
|
132
|
-
logger.error("Model compilation failed", {
|
|
133
|
-
packageName,
|
|
134
|
-
modelPath,
|
|
135
|
-
error: message,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
this.packageLoadHistogram.record(performance.now() - startTime, {
|
|
139
|
-
malloy_package_name: packageName,
|
|
140
|
-
status: "compilation_error",
|
|
141
|
-
});
|
|
142
|
-
throw err;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const endTime = performance.now();
|
|
146
|
-
const executionTime = endTime - startTime;
|
|
147
|
-
this.packageLoadHistogram.record(executionTime, {
|
|
148
|
-
malloy_package_name: packageName,
|
|
149
|
-
status: "success",
|
|
150
|
-
});
|
|
151
|
-
logger.info(`Successfully loaded package ${packageName}`, {
|
|
152
|
-
packageName,
|
|
153
|
-
duration: formatDuration(executionTime),
|
|
154
|
-
});
|
|
155
|
-
return new Package(
|
|
156
|
-
projectName,
|
|
157
|
-
packageName,
|
|
158
|
-
packagePath,
|
|
159
|
-
packageConfig,
|
|
160
|
-
databases,
|
|
161
|
-
models,
|
|
162
|
-
connections,
|
|
163
|
-
);
|
|
164
|
-
} catch (error) {
|
|
165
|
-
logger.error(`Error loading package ${packageName}`, { error });
|
|
166
|
-
console.error(error);
|
|
167
|
-
const endTime = performance.now();
|
|
168
|
-
const executionTime = endTime - startTime;
|
|
169
|
-
this.packageLoadHistogram.record(executionTime, {
|
|
170
|
-
malloy_package_name: packageName,
|
|
171
|
-
status: "error",
|
|
172
|
-
});
|
|
173
|
-
// Clean up package directory on failure
|
|
174
|
-
try {
|
|
175
|
-
await fs.rm(packagePath, {
|
|
176
|
-
recursive: true,
|
|
177
|
-
force: true,
|
|
178
|
-
});
|
|
179
|
-
logger.info(`Cleaned up failed package directory: ${packagePath}`);
|
|
180
|
-
} catch (cleanupError) {
|
|
181
|
-
logger.warn(`Failed to clean up package directory ${packagePath}`, {
|
|
182
|
-
error: cleanupError,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
public getPackageName(): string {
|
|
190
|
-
return this.packageName;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
public getPackagePath(): string {
|
|
194
|
-
return this.packagePath;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
public getPackageMetadata(): ApiPackage {
|
|
198
|
-
return this.packageMetadata;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
public listDatabases(): ApiDatabase[] {
|
|
202
|
-
return this.databases;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
public getModel(modelPath: string): Model | undefined {
|
|
206
|
-
return this.models.get(modelPath);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
public getModelPaths(): string[] {
|
|
210
|
-
return Array.from(this.models.keys());
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Recompile every model in the package with the given build manifest
|
|
215
|
-
* so queries resolve persist references to materialized tables.
|
|
216
|
-
*
|
|
217
|
-
* Builds a fresh map off to the side and swaps it in at the end. If any
|
|
218
|
-
* recompile fails the whole call rejects before the swap and the live
|
|
219
|
-
* `this.models` reference remains untouched — no half-loaded state is
|
|
220
|
-
* ever observable to concurrent readers.
|
|
221
|
-
*/
|
|
222
|
-
public async reloadAllModels(
|
|
223
|
-
buildManifest: BuildManifest["entries"],
|
|
224
|
-
): Promise<void> {
|
|
225
|
-
const modelPaths = Array.from(this.models.keys());
|
|
226
|
-
logger.info("Reloading all models with build manifest", {
|
|
227
|
-
packageName: this.packageName,
|
|
228
|
-
modelCount: modelPaths.length,
|
|
229
|
-
manifestEntryCount: Object.keys(buildManifest).length,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const reloaded = await Promise.all(
|
|
233
|
-
modelPaths.map((modelPath) =>
|
|
234
|
-
Model.create(
|
|
235
|
-
this.packageName,
|
|
236
|
-
this.packagePath,
|
|
237
|
-
modelPath,
|
|
238
|
-
this.connections,
|
|
239
|
-
{ buildManifest },
|
|
240
|
-
),
|
|
241
|
-
),
|
|
242
|
-
);
|
|
243
|
-
const nextModels = new Map<string, Model>();
|
|
244
|
-
for (const model of reloaded) {
|
|
245
|
-
nextModels.set(model.getPath(), model);
|
|
246
|
-
}
|
|
247
|
-
this.models = nextModels;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
public getConnections(): Map<string, Connection> {
|
|
251
|
-
return this.connections;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
public getMalloyConnection(connectionName: string): Connection {
|
|
255
|
-
const connection = this.connections.get(connectionName);
|
|
256
|
-
if (!connection) {
|
|
257
|
-
throw new Error(
|
|
258
|
-
`Connection ${connectionName} not found in package ${this.packageName}`,
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
return connection;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
public async getModelFileText(modelPath: string): Promise<string> {
|
|
265
|
-
const model = this.getModel(modelPath);
|
|
266
|
-
if (!model) {
|
|
267
|
-
throw new Error(`Model not found: ${modelPath}`);
|
|
268
|
-
}
|
|
269
|
-
return await model.getFileText(this.packagePath);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
public async listModels(): Promise<ApiModel[]> {
|
|
273
|
-
const values = await Promise.all(
|
|
274
|
-
Array.from(this.models.keys())
|
|
275
|
-
.filter((modelPath) => {
|
|
276
|
-
return modelPath.endsWith(MODEL_FILE_SUFFIX);
|
|
277
|
-
})
|
|
278
|
-
.map(async (modelPath) => {
|
|
279
|
-
let error: string | undefined;
|
|
280
|
-
if (ENABLE_LIST_MODEL_COMPILATION) {
|
|
281
|
-
try {
|
|
282
|
-
await this.models.get(modelPath)?.getModel();
|
|
283
|
-
} catch (modelError) {
|
|
284
|
-
error =
|
|
285
|
-
modelError instanceof Error
|
|
286
|
-
? modelError.message
|
|
287
|
-
: undefined;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return {
|
|
291
|
-
projectName: this.projectName,
|
|
292
|
-
path: modelPath,
|
|
293
|
-
packageName: this.packageName,
|
|
294
|
-
error,
|
|
295
|
-
};
|
|
296
|
-
}),
|
|
297
|
-
);
|
|
298
|
-
return values;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
public async listNotebooks(): Promise<ApiNotebook[]> {
|
|
302
|
-
return await Promise.all(
|
|
303
|
-
Array.from(this.models.keys())
|
|
304
|
-
.filter((modelPath) => {
|
|
305
|
-
return modelPath.endsWith(NOTEBOOK_FILE_SUFFIX);
|
|
306
|
-
})
|
|
307
|
-
.map(async (modelPath) => {
|
|
308
|
-
let error: Error | undefined;
|
|
309
|
-
if (ENABLE_LIST_MODEL_COMPILATION) {
|
|
310
|
-
error = this.models.get(modelPath)?.getNotebookError();
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
projectName: this.projectName,
|
|
314
|
-
packageName: this.packageName,
|
|
315
|
-
path: modelPath,
|
|
316
|
-
error: error?.message,
|
|
317
|
-
};
|
|
318
|
-
}),
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private static async loadModels(
|
|
323
|
-
packageName: string,
|
|
324
|
-
packagePath: string,
|
|
325
|
-
connections: Map<string, Connection>,
|
|
326
|
-
): Promise<Map<string, Model>> {
|
|
327
|
-
const modelPaths = await Package.getModelPaths(packagePath);
|
|
328
|
-
const models = await Promise.all(
|
|
329
|
-
modelPaths.map((modelPath) =>
|
|
330
|
-
Model.create(packageName, packagePath, modelPath, connections),
|
|
331
|
-
),
|
|
332
|
-
);
|
|
333
|
-
return new Map(models.map((model) => [model.getPath(), model]));
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
private static async getModelPaths(packagePath: string): Promise<string[]> {
|
|
337
|
-
let files = undefined;
|
|
338
|
-
try {
|
|
339
|
-
files = await recursive(packagePath);
|
|
340
|
-
} catch (error) {
|
|
341
|
-
logger.error(error);
|
|
342
|
-
throw new PackageNotFoundError(
|
|
343
|
-
`Package config for ${packagePath} does not exist.`,
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
return files
|
|
347
|
-
.map((fullPath: string) => {
|
|
348
|
-
return path.relative(packagePath, fullPath).replace(/\\/g, "/");
|
|
349
|
-
})
|
|
350
|
-
.filter(
|
|
351
|
-
(modelPath: string) =>
|
|
352
|
-
modelPath.endsWith(MODEL_FILE_SUFFIX) ||
|
|
353
|
-
modelPath.endsWith(NOTEBOOK_FILE_SUFFIX),
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private static async validatePackageManifestExistsOrThrowError(
|
|
358
|
-
packagePath: string,
|
|
359
|
-
) {
|
|
360
|
-
const packageConfigPath = path.join(packagePath, PACKAGE_MANIFEST_NAME);
|
|
361
|
-
try {
|
|
362
|
-
await fs.stat(packageConfigPath);
|
|
363
|
-
} catch {
|
|
364
|
-
logger.error(`Can't find ${packageConfigPath}`);
|
|
365
|
-
throw new PackageNotFoundError(
|
|
366
|
-
`Package manifest for ${packagePath} does not exist.`,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
private static async readPackageConfig(
|
|
372
|
-
packagePath: string,
|
|
373
|
-
): Promise<ApiPackage> {
|
|
374
|
-
const packageConfigPath = path.join(packagePath, PACKAGE_MANIFEST_NAME);
|
|
375
|
-
const packageConfigContents = await fs.readFile(packageConfigPath);
|
|
376
|
-
// TODO: Validate package manifest. Define manifest type in public API.
|
|
377
|
-
const packageManifest = JSON.parse(packageConfigContents.toString());
|
|
378
|
-
return {
|
|
379
|
-
name: packageManifest.name,
|
|
380
|
-
description: packageManifest.description,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
private static async readDatabases(
|
|
385
|
-
packagePath: string,
|
|
386
|
-
): Promise<ApiDatabase[]> {
|
|
387
|
-
return await Promise.all(
|
|
388
|
-
(await Package.getDatabasePaths(packagePath)).map(
|
|
389
|
-
async (databasePath) => {
|
|
390
|
-
const databaseInfo = await Package.getDatabaseInfo(
|
|
391
|
-
packagePath,
|
|
392
|
-
databasePath,
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
return {
|
|
396
|
-
path: databasePath,
|
|
397
|
-
info: databaseInfo,
|
|
398
|
-
type: "embedded",
|
|
399
|
-
};
|
|
400
|
-
},
|
|
401
|
-
),
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
private static async getDatabasePaths(
|
|
406
|
-
packagePath: string,
|
|
407
|
-
): Promise<string[]> {
|
|
408
|
-
let files = undefined;
|
|
409
|
-
files = await recursive(packagePath);
|
|
410
|
-
return files
|
|
411
|
-
.map((fullPath: string) => {
|
|
412
|
-
return path.relative(packagePath, fullPath).replace(/\\/g, "/");
|
|
413
|
-
})
|
|
414
|
-
.filter(
|
|
415
|
-
(modelPath: string) =>
|
|
416
|
-
modelPath.endsWith(".parquet") || modelPath.endsWith(".csv"),
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
private static async getDatabaseInfo(
|
|
421
|
-
packagePath: string,
|
|
422
|
-
databasePath: string,
|
|
423
|
-
): Promise<ApiTableDescription> {
|
|
424
|
-
const fullPath = path.join(packagePath, databasePath);
|
|
425
|
-
|
|
426
|
-
// Create a DuckDB source then:
|
|
427
|
-
// 1. Load the model and get the table schema from model
|
|
428
|
-
// 2. Run a query to get the row count from the table
|
|
429
|
-
const runtime = new ConnectionRuntime({
|
|
430
|
-
urlReader: new EmptyURLReader(),
|
|
431
|
-
connections: [new DuckDBConnection("duckdb")],
|
|
432
|
-
});
|
|
433
|
-
// Normalize path to use forward slashes for cross-platform compatibility
|
|
434
|
-
// DuckDB on Windows supports forward slashes, and this avoids escaping issues
|
|
435
|
-
const normalizedPath = fullPath.replace(/\\/g, "/");
|
|
436
|
-
const model = runtime.loadModel(
|
|
437
|
-
`source: temp is duckdb.table('${normalizedPath}')`,
|
|
438
|
-
);
|
|
439
|
-
const modelDef = await model.getModel();
|
|
440
|
-
const fields = (modelDef._modelDef.contents["temp"] as SourceDef).fields;
|
|
441
|
-
const schema = fields.map((field): ApiColumn => {
|
|
442
|
-
return { type: field.type, name: field.name };
|
|
443
|
-
});
|
|
444
|
-
const runner = model.loadQuery(
|
|
445
|
-
"run: temp->{aggregate: row_count is count()}",
|
|
446
|
-
);
|
|
447
|
-
const result = await runner.run();
|
|
448
|
-
const rowCount = result.data.value[0].row_count?.valueOf() as number;
|
|
449
|
-
return { name: databasePath, rowCount, columns: schema };
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
public setName(name: string) {
|
|
453
|
-
this.packageName = name;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
public setProjectName(projectName: string) {
|
|
457
|
-
this.projectName = projectName;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
public setPackageMetadata(packageMetadata: ApiPackage) {
|
|
461
|
-
this.packageMetadata = packageMetadata;
|
|
462
|
-
}
|
|
463
|
-
}
|