@malloy-publisher/server 0.0.225 → 0.0.227
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.md +2 -11
- package/dist/app/api-doc.yaml +29 -24
- package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
- package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
- package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
- package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
- package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
- package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
- package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
- package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
- package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
- package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
- package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
- package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
- package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +71 -15
- package/dist/package_load_worker.mjs +169 -40
- package/dist/server.mjs +782 -7050
- package/package.json +1 -4
- package/src/controller/package.controller.spec.ts +17 -17
- package/src/controller/package.controller.ts +7 -6
- package/src/health.ts +3 -8
- package/src/logger.spec.ts +193 -0
- package/src/logger.ts +115 -18
- package/src/mcp/error_messages.ts +8 -37
- package/src/mcp/handler_utils.ts +10 -129
- package/src/mcp/mcp_constants.ts +0 -16
- package/src/mcp/{agent_server.protocol.spec.ts → server.protocol.spec.ts} +14 -12
- package/src/mcp/server.ts +29 -37
- package/src/mcp/skills/build_skills_bundle.ts +1 -1
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/mcp/tools/docs_search_tool.ts +1 -1
- package/src/mcp/tools/execute_query_tool.ts +2 -2
- package/src/mcp/tools/get_context_eval.ts +3 -3
- package/src/mcp/tools/get_context_tool.spec.ts +196 -1
- package/src/mcp/tools/get_context_tool.ts +165 -67
- package/src/package_load/package_load_pool.ts +8 -1
- package/src/package_load/package_load_worker.ts +75 -24
- package/src/package_load/protocol.ts +21 -1
- package/src/package_load/rpc_wait_accountant.spec.ts +109 -0
- package/src/package_load/rpc_wait_accountant.ts +76 -0
- package/src/package_load_metrics.spec.ts +114 -0
- package/src/package_load_metrics.ts +127 -0
- package/src/pg_helpers.spec.ts +2 -206
- package/src/pg_helpers.ts +4 -120
- package/src/server.ts +0 -16
- package/src/service/build_plan.spec.ts +42 -60
- package/src/service/build_plan.ts +37 -61
- package/src/service/environment.ts +5 -1
- package/src/service/materialization_test_fixtures.ts +6 -8
- package/src/service/package.ts +165 -86
- package/src/service/package_manifest.spec.ts +22 -1
- package/src/service/package_manifest.ts +29 -0
- package/src/service/persistence_policy.spec.ts +234 -0
- package/src/test_helpers/metrics_harness.ts +40 -0
- package/tests/harness/mcp_test_setup.ts +1 -1
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +7 -31
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -6
- package/dxt/malloy_bridge.py +0 -354
- package/dxt/manifest.json +0 -22
- package/src/dto/connection.dto.spec.ts +0 -186
- package/src/dto/connection.dto.ts +0 -308
- package/src/dto/index.ts +0 -2
- package/src/dto/package.dto.spec.ts +0 -42
- package/src/dto/package.dto.ts +0 -27
- package/src/dto/validate.spec.ts +0 -76
- package/src/dto/validate.ts +0 -31
- package/src/ducklake_version.spec.ts +0 -43
- package/src/ducklake_version.ts +0 -26
- package/src/mcp/agent_server.spec.ts +0 -18
- package/src/mcp/agent_server.ts +0 -144
- 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/environment_resource.ts +0 -187
- package/src/mcp/resources/model_resource.ts +0 -155
- package/src/mcp/resources/notebook_resource.ts +0 -137
- package/src/mcp/resources/package_resource.ts +0 -373
- package/src/mcp/resources/query_resource.ts +0 -122
- package/src/mcp/resources/source_resource.ts +0 -141
- package/src/mcp/resources/view_resource.ts +0 -136
- package/src/mcp/tools/discovery_tools.ts +0 -280
- package/src/service/materialization_cron_gate.spec.ts +0 -181
- package/src/storage/BaseRepository.ts +0 -31
- package/src/storage/StorageManager.mock.ts +0 -50
- package/tests/harness/e2e.ts +0 -96
- package/tests/harness/mocks.ts +0 -39
- package/tests/harness/uris.ts +0 -31
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -655
- package/tests/integration/mcp/setup.spec.ts +0 -5
- package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
- package/tests/unit/mcp/prompt_happy.test.ts +0 -51
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
McpServer,
|
|
3
|
-
ResourceTemplate,
|
|
4
|
-
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import { URL } from "url";
|
|
6
|
-
import type { components } from "../../api"; // Need this for View definition type
|
|
7
|
-
import { ModelCompilationError } from "../../errors";
|
|
8
|
-
import { logger } from "../../logger";
|
|
9
|
-
import { EnvironmentStore } from "../../service/environment_store";
|
|
10
|
-
import {
|
|
11
|
-
getInternalError,
|
|
12
|
-
getMalloyErrorDetails,
|
|
13
|
-
getNotFoundError,
|
|
14
|
-
} from "../error_messages";
|
|
15
|
-
import {
|
|
16
|
-
getModelForQuery,
|
|
17
|
-
handleResourceGet,
|
|
18
|
-
McpGetResourceError,
|
|
19
|
-
} from "../handler_utils";
|
|
20
|
-
import { RESOURCE_METADATA } from "../resource_metadata";
|
|
21
|
-
|
|
22
|
-
// Define the expected parameter types
|
|
23
|
-
type ViewParams = {
|
|
24
|
-
environmentName?: unknown;
|
|
25
|
-
packageName?: unknown;
|
|
26
|
-
modelPath?: unknown;
|
|
27
|
-
sourceName?: unknown;
|
|
28
|
-
viewName?: unknown;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Registers the Malloy View resource type (nested within a Source).
|
|
33
|
-
*/
|
|
34
|
-
export function registerViewResource(
|
|
35
|
-
mcpServer: McpServer,
|
|
36
|
-
environmentStore: EnvironmentStore,
|
|
37
|
-
): void {
|
|
38
|
-
mcpServer.resource(
|
|
39
|
-
"view",
|
|
40
|
-
new ResourceTemplate(
|
|
41
|
-
"malloy://environment/{environmentName}/package/{packageName}/models/{modelPath}/sources/{sourceName}/views/{viewName}",
|
|
42
|
-
{ list: undefined }, // Listing views is not supported via this template
|
|
43
|
-
),
|
|
44
|
-
(uri, params) =>
|
|
45
|
-
handleResourceGet(
|
|
46
|
-
uri,
|
|
47
|
-
params as ViewParams,
|
|
48
|
-
"view",
|
|
49
|
-
async (
|
|
50
|
-
{
|
|
51
|
-
environmentName,
|
|
52
|
-
packageName,
|
|
53
|
-
modelPath,
|
|
54
|
-
sourceName,
|
|
55
|
-
viewName,
|
|
56
|
-
}: ViewParams,
|
|
57
|
-
uri: URL,
|
|
58
|
-
) => {
|
|
59
|
-
if (
|
|
60
|
-
typeof environmentName !== "string" ||
|
|
61
|
-
typeof packageName !== "string" ||
|
|
62
|
-
typeof modelPath !== "string" ||
|
|
63
|
-
typeof sourceName !== "string" ||
|
|
64
|
-
typeof viewName !== "string"
|
|
65
|
-
) {
|
|
66
|
-
throw new Error("Invalid parameters for view resource.");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const modelResult = await getModelForQuery(
|
|
71
|
-
environmentStore,
|
|
72
|
-
environmentName,
|
|
73
|
-
packageName,
|
|
74
|
-
modelPath,
|
|
75
|
-
);
|
|
76
|
-
if ("error" in modelResult) {
|
|
77
|
-
throw new McpGetResourceError(modelResult.error);
|
|
78
|
-
}
|
|
79
|
-
const { model } = modelResult;
|
|
80
|
-
|
|
81
|
-
const sources = await model.getSources();
|
|
82
|
-
if (!sources) {
|
|
83
|
-
throw new Error("Could not retrieve sources from model.");
|
|
84
|
-
}
|
|
85
|
-
const source = sources.find(
|
|
86
|
-
(s: components["schemas"]["Source"]) =>
|
|
87
|
-
s.name === sourceName,
|
|
88
|
-
);
|
|
89
|
-
if (!source) {
|
|
90
|
-
throw new McpGetResourceError(
|
|
91
|
-
getNotFoundError(
|
|
92
|
-
`Source '${sourceName}' in model '${modelPath}' package '${packageName}' environment '${environmentName}'`,
|
|
93
|
-
),
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Find the view within the source
|
|
98
|
-
const view = source.views?.find(
|
|
99
|
-
(v: components["schemas"]["View"]) => v.name === viewName,
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
if (!view) {
|
|
103
|
-
// Specific "View not found" error
|
|
104
|
-
const errorDetails = getNotFoundError(
|
|
105
|
-
`View '${viewName}' in source '${sourceName}' model '${modelPath}' package '${packageName}' environment '${environmentName}'`,
|
|
106
|
-
);
|
|
107
|
-
throw new McpGetResourceError(errorDetails);
|
|
108
|
-
}
|
|
109
|
-
return view;
|
|
110
|
-
} catch (error) {
|
|
111
|
-
if (error instanceof McpGetResourceError) {
|
|
112
|
-
throw error; // Re-throw already formatted
|
|
113
|
-
}
|
|
114
|
-
if (error instanceof ModelCompilationError) {
|
|
115
|
-
const errorDetails = getMalloyErrorDetails(
|
|
116
|
-
"GetResource (view - model compilation)",
|
|
117
|
-
`${environmentName}/${packageName}/${modelPath}`,
|
|
118
|
-
error,
|
|
119
|
-
);
|
|
120
|
-
throw new McpGetResourceError(errorDetails);
|
|
121
|
-
}
|
|
122
|
-
logger.error(
|
|
123
|
-
`[MCP Server Error] Error fetching view '${viewName}' from ${uri.href}:`,
|
|
124
|
-
{ error },
|
|
125
|
-
);
|
|
126
|
-
const fallbackErrorDetails = getInternalError(
|
|
127
|
-
`GetResource (view: ${uri.href})`,
|
|
128
|
-
error,
|
|
129
|
-
);
|
|
130
|
-
throw new McpGetResourceError(fallbackErrorDetails);
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
RESOURCE_METADATA.view,
|
|
134
|
-
),
|
|
135
|
-
);
|
|
136
|
-
}
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { EnvironmentStore } from "../../service/environment_store";
|
|
4
|
-
import { buildMalloyUri } from "../handler_utils";
|
|
5
|
-
|
|
6
|
-
const listPackagesShape = {
|
|
7
|
-
environmentName: z
|
|
8
|
-
.string()
|
|
9
|
-
.describe(
|
|
10
|
-
"Environment name. Names are listed in the malloy_environmentList tool.",
|
|
11
|
-
),
|
|
12
|
-
};
|
|
13
|
-
type listPackagesParams = z.infer<z.ZodObject<typeof listPackagesShape>>;
|
|
14
|
-
|
|
15
|
-
const getModelsShape = {
|
|
16
|
-
environmentName: z
|
|
17
|
-
.string()
|
|
18
|
-
.describe(
|
|
19
|
-
"Environment name. Names are listed in the malloy_environmentList tool.",
|
|
20
|
-
),
|
|
21
|
-
packageName: z
|
|
22
|
-
.string()
|
|
23
|
-
.describe(
|
|
24
|
-
"Package name. Package names are listed in the listPackages tool.",
|
|
25
|
-
),
|
|
26
|
-
};
|
|
27
|
-
type getModelsParams = z.infer<z.ZodObject<typeof getModelsShape>>;
|
|
28
|
-
|
|
29
|
-
const getModelTextShape = {
|
|
30
|
-
environmentName: z
|
|
31
|
-
.string()
|
|
32
|
-
.describe(
|
|
33
|
-
"Environment name. Names are listed in the malloy_environmentList tool.",
|
|
34
|
-
),
|
|
35
|
-
packageName: z
|
|
36
|
-
.string()
|
|
37
|
-
.describe(
|
|
38
|
-
"Package name. Package names are listed in the listPackages tool.",
|
|
39
|
-
),
|
|
40
|
-
modelPath: z
|
|
41
|
-
.string()
|
|
42
|
-
.describe(
|
|
43
|
-
"Model path. Model paths are listed in the malloy_packageGet tool.",
|
|
44
|
-
),
|
|
45
|
-
};
|
|
46
|
-
type getModelTextParams = z.infer<z.ZodObject<typeof getModelTextShape>>;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Registers Malloy discovery tools with the MCP server.
|
|
50
|
-
* Lists environments and packages for navigation.
|
|
51
|
-
*/
|
|
52
|
-
export function registerTools(
|
|
53
|
-
mcpServer: McpServer,
|
|
54
|
-
environmentStore: EnvironmentStore,
|
|
55
|
-
): void {
|
|
56
|
-
mcpServer.tool(
|
|
57
|
-
"malloy_environmentList",
|
|
58
|
-
"Lists all Malloy environments",
|
|
59
|
-
{},
|
|
60
|
-
async () => {
|
|
61
|
-
console.log(
|
|
62
|
-
"[MCP LOG] Entering ListResources (environment) handler (listing ALL packages)...",
|
|
63
|
-
);
|
|
64
|
-
const allEnvironments = await Promise.all(
|
|
65
|
-
(await environmentStore.listEnvironments())
|
|
66
|
-
.filter((environment) => environment.name)
|
|
67
|
-
.map((environment) => ({
|
|
68
|
-
name: environment.name,
|
|
69
|
-
environment: environmentStore.getEnvironment(
|
|
70
|
-
environment.name!,
|
|
71
|
-
false,
|
|
72
|
-
),
|
|
73
|
-
})),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
console.log(
|
|
77
|
-
`[MCP LOG] Found ${allEnvironments.length} environments defined.`,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const mappedResources = await Promise.all(
|
|
81
|
-
allEnvironments.map(async (environment) => {
|
|
82
|
-
const name = environment.name;
|
|
83
|
-
const environmentInstance = await environment.environment;
|
|
84
|
-
const metadata =
|
|
85
|
-
await environmentInstance.reloadEnvironmentMetadata();
|
|
86
|
-
const readme = metadata.readme;
|
|
87
|
-
return {
|
|
88
|
-
name,
|
|
89
|
-
type: "environment",
|
|
90
|
-
description: readme || "NO Description available",
|
|
91
|
-
};
|
|
92
|
-
}),
|
|
93
|
-
);
|
|
94
|
-
console.log(
|
|
95
|
-
`[MCP LOG] ListResources (environment): Returning ${mappedResources.length} package resources.`,
|
|
96
|
-
);
|
|
97
|
-
return {
|
|
98
|
-
content: [
|
|
99
|
-
{
|
|
100
|
-
type: "resource",
|
|
101
|
-
resource: {
|
|
102
|
-
type: "application/json",
|
|
103
|
-
uri: buildMalloyUri({}, "environment"),
|
|
104
|
-
text: JSON.stringify(mappedResources),
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
};
|
|
109
|
-
},
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
mcpServer.tool(
|
|
113
|
-
"malloy_packageList",
|
|
114
|
-
"Lists all Malloy packages within an environment",
|
|
115
|
-
listPackagesShape,
|
|
116
|
-
async (params: listPackagesParams) => {
|
|
117
|
-
const { environmentName } = params;
|
|
118
|
-
console.log(
|
|
119
|
-
"[MCP LOG] Entering ListResources (environment) handler (listing ALL packages)...",
|
|
120
|
-
);
|
|
121
|
-
const environment = await environmentStore.getEnvironment(
|
|
122
|
-
environmentName,
|
|
123
|
-
false,
|
|
124
|
-
);
|
|
125
|
-
const packages = await environment.listPackages();
|
|
126
|
-
console.log(`[MCP LOG] Found ${packages.length} packages defined.`);
|
|
127
|
-
const mappedResources = packages.map((pkg) => ({
|
|
128
|
-
modelPath: pkg.name,
|
|
129
|
-
type: "package",
|
|
130
|
-
description: pkg.description,
|
|
131
|
-
}));
|
|
132
|
-
console.log(
|
|
133
|
-
`[MCP LOG] ListResources (environment): Returning ${mappedResources.length} package resources.`,
|
|
134
|
-
);
|
|
135
|
-
return {
|
|
136
|
-
content: [
|
|
137
|
-
{
|
|
138
|
-
type: "resource",
|
|
139
|
-
resource: {
|
|
140
|
-
type: "application/json",
|
|
141
|
-
uri: buildMalloyUri(
|
|
142
|
-
{ environment: environmentName },
|
|
143
|
-
"package",
|
|
144
|
-
),
|
|
145
|
-
text: JSON.stringify(mappedResources),
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
};
|
|
150
|
-
},
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
mcpServer.tool(
|
|
154
|
-
"malloy_packageGet",
|
|
155
|
-
"Lists resources within a package",
|
|
156
|
-
getModelsShape,
|
|
157
|
-
async (params: getModelsParams) => {
|
|
158
|
-
const { environmentName, packageName } = params;
|
|
159
|
-
console.log(
|
|
160
|
-
"[MCP LOG] Entering GetResources (environment) handler (listing ALL packages)...",
|
|
161
|
-
);
|
|
162
|
-
const environment = await environmentStore.getEnvironment(
|
|
163
|
-
environmentName,
|
|
164
|
-
false,
|
|
165
|
-
);
|
|
166
|
-
const pkg = await environment.getPackage(packageName, false);
|
|
167
|
-
const models = await pkg.listModels();
|
|
168
|
-
console.log(`[MCP LOG] Found ${models.length} models defined.`);
|
|
169
|
-
const mappedResources = models.map((model) => ({
|
|
170
|
-
name: model.path,
|
|
171
|
-
type: "model",
|
|
172
|
-
}));
|
|
173
|
-
console.log(
|
|
174
|
-
`[MCP LOG] ListResources (environment): Returning ${mappedResources.length} package resources.`,
|
|
175
|
-
);
|
|
176
|
-
return {
|
|
177
|
-
content: [
|
|
178
|
-
{
|
|
179
|
-
type: "resource",
|
|
180
|
-
resource: {
|
|
181
|
-
type: "application/json",
|
|
182
|
-
uri: buildMalloyUri(
|
|
183
|
-
{
|
|
184
|
-
environment: environmentName,
|
|
185
|
-
package: packageName,
|
|
186
|
-
},
|
|
187
|
-
"model",
|
|
188
|
-
),
|
|
189
|
-
text: JSON.stringify(mappedResources),
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
};
|
|
194
|
-
},
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
mcpServer.tool(
|
|
198
|
-
"malloy_modelGetText",
|
|
199
|
-
"Gets the raw text content of a model file",
|
|
200
|
-
getModelTextShape,
|
|
201
|
-
async (params: getModelTextParams) => {
|
|
202
|
-
const { environmentName, packageName, modelPath } = params;
|
|
203
|
-
console.log(
|
|
204
|
-
`[MCP LOG] Entering GetModelText handler for ${environmentName}/${packageName}/${modelPath}...`,
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
const environment = await environmentStore.getEnvironment(
|
|
209
|
-
environmentName,
|
|
210
|
-
false,
|
|
211
|
-
);
|
|
212
|
-
const pkg = await environment.getPackage(packageName, false);
|
|
213
|
-
const model = pkg.getModel(modelPath);
|
|
214
|
-
|
|
215
|
-
if (!model) {
|
|
216
|
-
console.log(
|
|
217
|
-
"model not found",
|
|
218
|
-
modelPath,
|
|
219
|
-
"in ",
|
|
220
|
-
pkg.listModels(),
|
|
221
|
-
);
|
|
222
|
-
throw new Error(`Model not found: ${modelPath}`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Route through the Environment so the disk read is serialized
|
|
226
|
-
// against installPackage / deletePackage.
|
|
227
|
-
const fileText = await environment.getModelFileText(
|
|
228
|
-
packageName,
|
|
229
|
-
modelPath,
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
console.log(
|
|
233
|
-
`[MCP LOG] Successfully retrieved model text for ${modelPath}`,
|
|
234
|
-
);
|
|
235
|
-
return {
|
|
236
|
-
content: [
|
|
237
|
-
{
|
|
238
|
-
type: "resource",
|
|
239
|
-
resource: {
|
|
240
|
-
type: "text/plain",
|
|
241
|
-
uri: buildMalloyUri(
|
|
242
|
-
{
|
|
243
|
-
environment: environmentName,
|
|
244
|
-
package: packageName,
|
|
245
|
-
model: modelPath,
|
|
246
|
-
},
|
|
247
|
-
"model-text",
|
|
248
|
-
),
|
|
249
|
-
text: fileText,
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
],
|
|
253
|
-
};
|
|
254
|
-
} catch (error) {
|
|
255
|
-
console.error(`[MCP LOG] Error retrieving model text: ${error}`);
|
|
256
|
-
const errorMessage =
|
|
257
|
-
error instanceof Error ? error.message : "Unknown error";
|
|
258
|
-
return {
|
|
259
|
-
content: [
|
|
260
|
-
{
|
|
261
|
-
type: "resource",
|
|
262
|
-
resource: {
|
|
263
|
-
type: "text/plain",
|
|
264
|
-
uri: buildMalloyUri(
|
|
265
|
-
{
|
|
266
|
-
environment: environmentName,
|
|
267
|
-
package: packageName,
|
|
268
|
-
model: modelPath,
|
|
269
|
-
},
|
|
270
|
-
"model-text",
|
|
271
|
-
),
|
|
272
|
-
text: `Error: ${errorMessage}`,
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
],
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import * as fs from "fs/promises";
|
|
3
|
-
import * as os from "os";
|
|
4
|
-
import * as path from "path";
|
|
5
|
-
|
|
6
|
-
import { Environment } from "./environment";
|
|
7
|
-
import type { Package } from "./package";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The publish gate for materialization crons (Phase A carve-out,
|
|
11
|
-
* persistence.md §9.4). A cron is the power tier of artifact-anchored
|
|
12
|
-
* scheduling and is valid only over `sharing="private"` artifacts:
|
|
13
|
-
*
|
|
14
|
-
* - a package-level cron (`materialization.schedule`) is valid only when
|
|
15
|
-
* EVERY governed persist source resolves to explicit `sharing="private"`;
|
|
16
|
-
* - a per-source cron (`#@ persist ... schedule=`) is valid only when THAT
|
|
17
|
-
* source resolves to explicit `sharing="private"`;
|
|
18
|
-
* - a cron on a shared/unset artifact is rejected, pointing at
|
|
19
|
-
* `freshness.window` as the shared-tier replacement.
|
|
20
|
-
*
|
|
21
|
-
* These tests run a real `Environment` + `Package.create` over temp dirs, so
|
|
22
|
-
* they also prove end-to-end that the pinned compiler ACCEPTS `sharing=` /
|
|
23
|
-
* `refresh=` / `schedule=` / `freshness.*` keys in the `#@ persist` annotation
|
|
24
|
-
* and that the values survive to the wire build plan verbatim (unset stays null
|
|
25
|
-
* — never defaulted to "shared").
|
|
26
|
-
*/
|
|
27
|
-
describe("materialization cron gate", () => {
|
|
28
|
-
let rootDir: string;
|
|
29
|
-
let envPath: string;
|
|
30
|
-
|
|
31
|
-
async function loadPackage(model: string, schedule?: string) {
|
|
32
|
-
const dir = path.join(envPath, "pkg");
|
|
33
|
-
await fs.mkdir(dir, { recursive: true });
|
|
34
|
-
await fs.writeFile(
|
|
35
|
-
path.join(dir, "publisher.json"),
|
|
36
|
-
JSON.stringify({
|
|
37
|
-
name: "pkg",
|
|
38
|
-
description: "fixture",
|
|
39
|
-
...(schedule ? { materialization: { schedule } } : {}),
|
|
40
|
-
}),
|
|
41
|
-
);
|
|
42
|
-
await fs.writeFile(path.join(dir, "model.malloy"), model);
|
|
43
|
-
const env = await Environment.create("testEnv", envPath, []);
|
|
44
|
-
await env.addPackage("pkg");
|
|
45
|
-
return env.getPackage("pkg", false);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function sourceByName(pkg: Package, name: string) {
|
|
49
|
-
const sources = pkg.getBuildPlan()?.sources ?? {};
|
|
50
|
-
const found = Object.values(sources).find((s) => s.name === name);
|
|
51
|
-
if (!found) throw new Error(`persist source '${name}' not in build plan`);
|
|
52
|
-
return found;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
beforeEach(async () => {
|
|
56
|
-
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "publisher-cron-"));
|
|
57
|
-
envPath = path.join(rootDir, "env");
|
|
58
|
-
await fs.mkdir(envPath, { recursive: true });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
afterEach(async () => {
|
|
62
|
-
await fs.rm(rootDir, { recursive: true, force: true }).catch(() => {});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const MIXED_MODEL = `##! experimental.persistence
|
|
66
|
-
|
|
67
|
-
#@ persist name="priv_table" sharing=private refresh=incremental
|
|
68
|
-
source: priv is duckdb.sql("SELECT 1 as x")
|
|
69
|
-
|
|
70
|
-
#@ persist name="open_table" sharing=shared
|
|
71
|
-
source: open is duckdb.sql("SELECT 2 as x")
|
|
72
|
-
|
|
73
|
-
#@ persist name="unset_table"
|
|
74
|
-
source: unspecified is duckdb.sql("SELECT 3 as x")
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
|
-
const ALL_PRIVATE_MODEL = `##! experimental.persistence
|
|
78
|
-
|
|
79
|
-
#@ persist name="a_table" sharing=private
|
|
80
|
-
source: a is duckdb.sql("SELECT 1 as x")
|
|
81
|
-
|
|
82
|
-
#@ persist name="b_table" sharing=private refresh=full
|
|
83
|
-
source: b is duckdb.sql("SELECT 2 as x")
|
|
84
|
-
`;
|
|
85
|
-
|
|
86
|
-
// Per-source crons: valid on the private source, rejected on the shared and
|
|
87
|
-
// the unset (⇒ shared) sources.
|
|
88
|
-
const PER_SOURCE_CRON_MODEL = `##! experimental.persistence
|
|
89
|
-
|
|
90
|
-
#@ persist name="p_table" sharing=private schedule="0 */6 * * *"
|
|
91
|
-
source: p is duckdb.sql("SELECT 1 as x")
|
|
92
|
-
|
|
93
|
-
#@ persist name="s_table" sharing=shared schedule="0 0 * * *"
|
|
94
|
-
source: s is duckdb.sql("SELECT 2 as x")
|
|
95
|
-
|
|
96
|
-
#@ persist name="u_table" schedule="0 0 * * *"
|
|
97
|
-
source: u is duckdb.sql("SELECT 3 as x")
|
|
98
|
-
`;
|
|
99
|
-
|
|
100
|
-
it(
|
|
101
|
-
"surfaces declared sharing/refresh verbatim on the build plan (null when unset)",
|
|
102
|
-
async () => {
|
|
103
|
-
const pkg = await loadPackage(MIXED_MODEL);
|
|
104
|
-
|
|
105
|
-
const priv = sourceByName(pkg, "priv");
|
|
106
|
-
expect(priv.sharing).toBe("private");
|
|
107
|
-
expect(priv.refresh).toBe("incremental");
|
|
108
|
-
|
|
109
|
-
const open = sourceByName(pkg, "open");
|
|
110
|
-
expect(open.sharing).toBe("shared");
|
|
111
|
-
expect(open.refresh).toBeNull();
|
|
112
|
-
|
|
113
|
-
// Unset must be reported as null — distinguishable from an explicit
|
|
114
|
-
// "shared" — because the control plane applies the platform default
|
|
115
|
-
// (unset => shared) itself.
|
|
116
|
-
const unspecified = sourceByName(pkg, "unspecified");
|
|
117
|
-
expect(unspecified.sharing).toBeNull();
|
|
118
|
-
expect(unspecified.refresh).toBeNull();
|
|
119
|
-
},
|
|
120
|
-
{ timeout: 30000 },
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
it(
|
|
124
|
-
"rejects a package cron on a mixed package, pointing at freshness.window",
|
|
125
|
-
async () => {
|
|
126
|
-
// Phase A: a package cron governs every persist source, so it is valid
|
|
127
|
-
// only when all resolve to explicit private. MIXED has a shared and an
|
|
128
|
-
// unset source, so the package cron is rejected.
|
|
129
|
-
const pkg = await loadPackage(MIXED_MODEL, "0 6 * * *");
|
|
130
|
-
|
|
131
|
-
const warnings = pkg.scheduleWarnings();
|
|
132
|
-
expect(warnings).toHaveLength(1);
|
|
133
|
-
const joined = pkg.formatInvalidSchedule();
|
|
134
|
-
expect(joined).toContain("materialization.schedule");
|
|
135
|
-
expect(joined).toContain("materialization.freshness.window");
|
|
136
|
-
},
|
|
137
|
-
{ timeout: 30000 },
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
it(
|
|
141
|
-
"accepts a package cron when every persist source is private",
|
|
142
|
-
async () => {
|
|
143
|
-
// Phase A carve-out: an all-private package legitimately carries a
|
|
144
|
-
// package-level cron (the power tier for its own private artifacts).
|
|
145
|
-
const pkg = await loadPackage(ALL_PRIVATE_MODEL, "0 6 * * *");
|
|
146
|
-
expect(pkg.scheduleWarnings()).toEqual([]);
|
|
147
|
-
},
|
|
148
|
-
{ timeout: 30000 },
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
it(
|
|
152
|
-
"rejects a per-source cron unless that source resolves to private",
|
|
153
|
-
async () => {
|
|
154
|
-
// No package cron here — only per-source `schedule=` declarations. The
|
|
155
|
-
// private source's cron is accepted; the shared and unset sources' are
|
|
156
|
-
// rejected, each pointing at freshness.window.
|
|
157
|
-
const pkg = await loadPackage(PER_SOURCE_CRON_MODEL);
|
|
158
|
-
|
|
159
|
-
const warnings = pkg.scheduleWarnings();
|
|
160
|
-
expect(warnings).toHaveLength(2);
|
|
161
|
-
const joined = warnings.join("\n");
|
|
162
|
-
expect(joined).toContain('"s"');
|
|
163
|
-
expect(joined).toContain('"u"');
|
|
164
|
-
expect(joined).not.toContain('"p"');
|
|
165
|
-
expect(joined).toContain("freshness.window");
|
|
166
|
-
|
|
167
|
-
// The resolved per-source schedule is surfaced verbatim on the plan.
|
|
168
|
-
expect(sourceByName(pkg, "p").schedule).toBe("0 */6 * * *");
|
|
169
|
-
},
|
|
170
|
-
{ timeout: 30000 },
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
it(
|
|
174
|
-
"is inert when no cron is declared, whatever the sources' sharing",
|
|
175
|
-
async () => {
|
|
176
|
-
const pkg = await loadPackage(MIXED_MODEL);
|
|
177
|
-
expect(pkg.scheduleWarnings()).toEqual([]);
|
|
178
|
-
},
|
|
179
|
-
{ timeout: 30000 },
|
|
180
|
-
);
|
|
181
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { DatabaseConnection } from "./DatabaseInterface";
|
|
2
|
-
|
|
3
|
-
export abstract class BaseRepository {
|
|
4
|
-
constructor(protected db: DatabaseConnection) {}
|
|
5
|
-
|
|
6
|
-
protected generateId(): string {
|
|
7
|
-
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
protected now(): Date {
|
|
11
|
-
return new Date();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
protected async executeQuery<T>(
|
|
15
|
-
_query: string,
|
|
16
|
-
_params?: unknown[],
|
|
17
|
-
): Promise<T[]> {
|
|
18
|
-
// This will be implemented by specific database implementations
|
|
19
|
-
throw new Error(
|
|
20
|
-
"executeQuery must be implemented by database-specific class",
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
protected async executeOne<T>(
|
|
25
|
-
query: string,
|
|
26
|
-
params?: unknown[],
|
|
27
|
-
): Promise<T | null> {
|
|
28
|
-
const results = await this.executeQuery<T>(query, params);
|
|
29
|
-
return results.length > 0 ? results[0] : null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
type MockData = Record<string, unknown>;
|
|
2
|
-
|
|
3
|
-
export class StorageManager {
|
|
4
|
-
async initialize(): Promise<void> {
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
getRepository() {
|
|
9
|
-
return {
|
|
10
|
-
listEnvironments: async (): Promise<unknown[]> => [],
|
|
11
|
-
createEnvironment: async (data: MockData): Promise<MockData> => ({
|
|
12
|
-
id: "test-id",
|
|
13
|
-
...data,
|
|
14
|
-
}),
|
|
15
|
-
updateEnvironment: async (
|
|
16
|
-
id: string,
|
|
17
|
-
data: MockData,
|
|
18
|
-
): Promise<MockData> => ({
|
|
19
|
-
id,
|
|
20
|
-
...data,
|
|
21
|
-
}),
|
|
22
|
-
getPackages: async (): Promise<unknown[]> => [],
|
|
23
|
-
createPackage: async (data: MockData): Promise<MockData> => ({
|
|
24
|
-
id: "test-id",
|
|
25
|
-
...data,
|
|
26
|
-
}),
|
|
27
|
-
updatePackage: async (
|
|
28
|
-
id: string,
|
|
29
|
-
data: MockData,
|
|
30
|
-
): Promise<MockData> => ({
|
|
31
|
-
id,
|
|
32
|
-
...data,
|
|
33
|
-
}),
|
|
34
|
-
deletePackage: async (): Promise<void> => {},
|
|
35
|
-
getConnections: async (): Promise<unknown[]> => [],
|
|
36
|
-
createConnection: async (data: MockData): Promise<MockData> => ({
|
|
37
|
-
id: "test-id",
|
|
38
|
-
...data,
|
|
39
|
-
}),
|
|
40
|
-
updateConnection: async (
|
|
41
|
-
id: string,
|
|
42
|
-
data: MockData,
|
|
43
|
-
): Promise<MockData> => ({
|
|
44
|
-
id,
|
|
45
|
-
...data,
|
|
46
|
-
}),
|
|
47
|
-
deleteConnection: async (): Promise<void> => {},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|