@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.
Files changed (98) hide show
  1. package/README.md +2 -11
  2. package/dist/app/api-doc.yaml +29 -24
  3. package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
  4. package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
  5. package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
  6. package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
  7. package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
  8. package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
  9. package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
  10. package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
  11. package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
  12. package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
  13. package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
  14. package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
  15. package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
  16. package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
  17. package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
  18. package/dist/app/index.html +1 -1
  19. package/dist/instrumentation.mjs +71 -15
  20. package/dist/package_load_worker.mjs +169 -40
  21. package/dist/server.mjs +782 -7050
  22. package/package.json +1 -4
  23. package/src/controller/package.controller.spec.ts +17 -17
  24. package/src/controller/package.controller.ts +7 -6
  25. package/src/health.ts +3 -8
  26. package/src/logger.spec.ts +193 -0
  27. package/src/logger.ts +115 -18
  28. package/src/mcp/error_messages.ts +8 -37
  29. package/src/mcp/handler_utils.ts +10 -129
  30. package/src/mcp/mcp_constants.ts +0 -16
  31. package/src/mcp/{agent_server.protocol.spec.ts → server.protocol.spec.ts} +14 -12
  32. package/src/mcp/server.ts +29 -37
  33. package/src/mcp/skills/build_skills_bundle.ts +1 -1
  34. package/src/mcp/skills/skills_bundle.json +1 -1
  35. package/src/mcp/tools/docs_search_tool.ts +1 -1
  36. package/src/mcp/tools/execute_query_tool.ts +2 -2
  37. package/src/mcp/tools/get_context_eval.ts +3 -3
  38. package/src/mcp/tools/get_context_tool.spec.ts +196 -1
  39. package/src/mcp/tools/get_context_tool.ts +165 -67
  40. package/src/package_load/package_load_pool.ts +8 -1
  41. package/src/package_load/package_load_worker.ts +75 -24
  42. package/src/package_load/protocol.ts +21 -1
  43. package/src/package_load/rpc_wait_accountant.spec.ts +109 -0
  44. package/src/package_load/rpc_wait_accountant.ts +76 -0
  45. package/src/package_load_metrics.spec.ts +114 -0
  46. package/src/package_load_metrics.ts +127 -0
  47. package/src/pg_helpers.spec.ts +2 -206
  48. package/src/pg_helpers.ts +4 -120
  49. package/src/server.ts +0 -16
  50. package/src/service/build_plan.spec.ts +42 -60
  51. package/src/service/build_plan.ts +37 -61
  52. package/src/service/environment.ts +5 -1
  53. package/src/service/materialization_test_fixtures.ts +6 -8
  54. package/src/service/package.ts +165 -86
  55. package/src/service/package_manifest.spec.ts +22 -1
  56. package/src/service/package_manifest.ts +29 -0
  57. package/src/service/persistence_policy.spec.ts +234 -0
  58. package/src/test_helpers/metrics_harness.ts +40 -0
  59. package/tests/harness/mcp_test_setup.ts +1 -1
  60. package/tests/integration/mcp/mcp_transport.integration.spec.ts +7 -31
  61. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -6
  62. package/dxt/malloy_bridge.py +0 -354
  63. package/dxt/manifest.json +0 -22
  64. package/src/dto/connection.dto.spec.ts +0 -186
  65. package/src/dto/connection.dto.ts +0 -308
  66. package/src/dto/index.ts +0 -2
  67. package/src/dto/package.dto.spec.ts +0 -42
  68. package/src/dto/package.dto.ts +0 -27
  69. package/src/dto/validate.spec.ts +0 -76
  70. package/src/dto/validate.ts +0 -31
  71. package/src/ducklake_version.spec.ts +0 -43
  72. package/src/ducklake_version.ts +0 -26
  73. package/src/mcp/agent_server.spec.ts +0 -18
  74. package/src/mcp/agent_server.ts +0 -144
  75. package/src/mcp/prompts/handlers.ts +0 -84
  76. package/src/mcp/prompts/index.ts +0 -11
  77. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  78. package/src/mcp/prompts/prompt_service.ts +0 -67
  79. package/src/mcp/prompts/utils.ts +0 -62
  80. package/src/mcp/resource_metadata.ts +0 -47
  81. package/src/mcp/resources/environment_resource.ts +0 -187
  82. package/src/mcp/resources/model_resource.ts +0 -155
  83. package/src/mcp/resources/notebook_resource.ts +0 -137
  84. package/src/mcp/resources/package_resource.ts +0 -373
  85. package/src/mcp/resources/query_resource.ts +0 -122
  86. package/src/mcp/resources/source_resource.ts +0 -141
  87. package/src/mcp/resources/view_resource.ts +0 -136
  88. package/src/mcp/tools/discovery_tools.ts +0 -280
  89. package/src/service/materialization_cron_gate.spec.ts +0 -181
  90. package/src/storage/BaseRepository.ts +0 -31
  91. package/src/storage/StorageManager.mock.ts +0 -50
  92. package/tests/harness/e2e.ts +0 -96
  93. package/tests/harness/mocks.ts +0 -39
  94. package/tests/harness/uris.ts +0 -31
  95. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -655
  96. package/tests/integration/mcp/setup.spec.ts +0 -5
  97. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  98. 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
- }