@malloy-publisher/server 0.0.195 → 0.0.196
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/dist/app/api-doc.yaml +213 -214
- package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +1 -0
- package/dist/app/assets/HomePage-DMop21VG.js +1 -0
- package/dist/app/assets/MainPage-BbE8ETz1.js +2 -0
- package/dist/app/assets/ModelPage-D2jvfe3t.js +1 -0
- package/dist/app/assets/PackagePage-BbnhGoD3.js +1 -0
- package/dist/app/assets/{RouteError-DefbDO7F.js → RouteError-D3LGEZ3i.js} +1 -1
- package/dist/app/assets/WorkbookPage-DttVIj4u.js +1 -0
- package/dist/app/assets/{core-BrfQApxh.es-DnvCX4oH.js → core-w79IMXAG.es-Bd0UlzOL.js} +1 -1
- package/dist/app/assets/{index-Bu0ub036.js → index-5K9YjIxF.js} +117 -117
- package/dist/app/assets/{index-CkzK3JIl.js → index-C513UodQ.js} +1 -1
- package/dist/app/assets/{index-CoA6HIGS.js → index-DIgzgp69.js} +1 -1
- package/dist/app/assets/{index.umd-B6Ms2PpL.js → index.umd-BMeMPq_9.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +1976 -1322
- package/package.json +2 -2
- package/publisher.config.json +2 -2
- package/src/config.spec.ts +181 -66
- package/src/config.ts +68 -47
- package/src/controller/compile.controller.ts +10 -7
- package/src/controller/connection.controller.ts +79 -58
- package/src/controller/database.controller.ts +10 -7
- package/src/controller/manifest.controller.ts +23 -14
- package/src/controller/materialization.controller.ts +14 -14
- package/src/controller/model.controller.ts +35 -20
- package/src/controller/package.controller.ts +83 -49
- package/src/controller/query.controller.ts +11 -8
- package/src/controller/watch-mode.controller.ts +35 -29
- package/src/errors.ts +2 -2
- package/src/mcp/error_messages.ts +2 -2
- package/src/mcp/handler_utils.ts +23 -20
- package/src/mcp/mcp_constants.ts +1 -1
- package/src/mcp/prompts/handlers.ts +3 -3
- package/src/mcp/prompts/prompt_service.ts +5 -5
- package/src/mcp/prompts/utils.ts +12 -12
- package/src/mcp/resource_metadata.ts +3 -3
- package/src/mcp/resources/environment_resource.ts +187 -0
- package/src/mcp/resources/model_resource.ts +19 -17
- package/src/mcp/resources/notebook_resource.ts +13 -13
- package/src/mcp/resources/package_resource.ts +30 -27
- package/src/mcp/resources/query_resource.ts +15 -10
- package/src/mcp/resources/source_resource.ts +10 -10
- package/src/mcp/resources/view_resource.ts +11 -11
- package/src/mcp/server.ts +16 -14
- package/src/mcp/tools/discovery_tools.ts +67 -49
- package/src/mcp/tools/execute_query_tool.ts +14 -14
- package/src/server-old.ts +1119 -0
- package/src/server.ts +191 -159
- package/src/service/connection.spec.ts +158 -133
- package/src/service/connection.ts +42 -39
- package/src/service/connection_config.spec.ts +13 -11
- package/src/service/connection_config.ts +28 -19
- package/src/service/connection_service.spec.ts +63 -43
- package/src/service/connection_service.ts +106 -89
- package/src/service/{project.ts → environment.ts} +92 -77
- package/src/service/{project_compile.spec.ts → environment_compile.spec.ts} +1 -1
- package/src/service/{project_store.spec.ts → environment_store.spec.ts} +99 -83
- package/src/service/{project_store.ts → environment_store.ts} +373 -327
- package/src/service/manifest_service.spec.ts +15 -15
- package/src/service/manifest_service.ts +26 -21
- package/src/service/materialization_service.spec.ts +93 -59
- package/src/service/materialization_service.ts +71 -62
- package/src/service/materialized_table_gc.spec.ts +15 -15
- package/src/service/materialized_table_gc.ts +3 -3
- package/src/service/model.ts +4 -4
- package/src/service/package.spec.ts +2 -2
- package/src/service/package.ts +23 -21
- package/src/service/resolve_environment.ts +15 -0
- package/src/storage/DatabaseInterface.ts +34 -25
- package/src/storage/StorageManager.mock.ts +3 -3
- package/src/storage/StorageManager.ts +64 -28
- package/src/storage/duckdb/ConnectionRepository.ts +13 -11
- package/src/storage/duckdb/DuckDBConnection.ts +1 -1
- package/src/storage/duckdb/DuckDBManifestStore.ts +6 -6
- package/src/storage/duckdb/DuckDBRepository.ts +47 -47
- package/src/storage/duckdb/{ProjectRepository.ts → EnvironmentRepository.ts} +35 -35
- package/src/storage/duckdb/ManifestRepository.ts +21 -20
- package/src/storage/duckdb/MaterializationRepository.ts +31 -28
- package/src/storage/duckdb/PackageRepository.ts +11 -11
- package/src/storage/duckdb/manifest_store.spec.ts +2 -2
- package/src/storage/duckdb/schema.ts +61 -20
- package/src/storage/ducklake/DuckLakeManifestStore.ts +20 -11
- package/tests/fixtures/publisher.config.json +1 -1
- package/tests/harness/e2e.ts +1 -1
- package/tests/harness/mcp_test_setup.ts +12 -24
- package/tests/harness/mocks.ts +10 -8
- package/tests/harness/rest_e2e.ts +2 -2
- package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +259 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +4 -4
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +28 -49
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +39 -47
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +1 -1
- package/tests/unit/duckdb/attached_databases.test.ts +51 -33
- package/tests/unit/duckdb/legacy_schema_migration.test.ts +194 -0
- package/tests/unit/ducklake/ducklake.test.ts +24 -22
- package/tests/unit/mcp/prompt_happy.test.ts +8 -8
- package/dist/app/assets/HomePage-DbZS0N7G.js +0 -1
- package/dist/app/assets/MainPage-CBuWkbmr.js +0 -2
- package/dist/app/assets/ModelPage-Bt37smot.js +0 -1
- package/dist/app/assets/PackagePage-DLZe50WG.js +0 -1
- package/dist/app/assets/ProjectPage-FQTEPXP4.js +0 -1
- package/dist/app/assets/WorkbookPage-CkAo16ar.js +0 -1
- package/src/mcp/resources/project_resource.ts +0 -184
- package/src/service/resolve_project.ts +0 -13
|
@@ -12,10 +12,10 @@ export class PackageRepository {
|
|
|
12
12
|
return new Date();
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
async listPackages(
|
|
15
|
+
async listPackages(environmentId: string): Promise<Package[]> {
|
|
16
16
|
const rows = await this.db.all<Record<string, unknown>>(
|
|
17
|
-
"SELECT * FROM packages WHERE
|
|
18
|
-
[
|
|
17
|
+
"SELECT * FROM packages WHERE environment_id = ? ORDER BY name",
|
|
18
|
+
[environmentId],
|
|
19
19
|
);
|
|
20
20
|
return rows.map(this.mapToPackage);
|
|
21
21
|
}
|
|
@@ -29,12 +29,12 @@ export class PackageRepository {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async getPackageByName(
|
|
32
|
-
|
|
32
|
+
environmentId: string,
|
|
33
33
|
name: string,
|
|
34
34
|
): Promise<Package | null> {
|
|
35
35
|
const row = await this.db.get<Record<string, unknown>>(
|
|
36
|
-
"SELECT * FROM packages WHERE
|
|
37
|
-
[
|
|
36
|
+
"SELECT * FROM packages WHERE environment_id = ? AND name = ?",
|
|
37
|
+
[environmentId, name],
|
|
38
38
|
);
|
|
39
39
|
return row ? this.mapToPackage(row) : null;
|
|
40
40
|
}
|
|
@@ -46,11 +46,11 @@ export class PackageRepository {
|
|
|
46
46
|
const now = this.now();
|
|
47
47
|
|
|
48
48
|
await this.db.run(
|
|
49
|
-
`INSERT INTO packages (id,
|
|
49
|
+
`INSERT INTO packages (id, environment_id, name, description, manifest_path, metadata, created_at, updated_at)
|
|
50
50
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
51
51
|
[
|
|
52
52
|
id,
|
|
53
|
-
pkg.
|
|
53
|
+
pkg.environmentId,
|
|
54
54
|
pkg.name,
|
|
55
55
|
pkg.description || null,
|
|
56
56
|
pkg.manifestPath,
|
|
@@ -114,14 +114,14 @@ export class PackageRepository {
|
|
|
114
114
|
await this.db.run("DELETE FROM packages WHERE id = ?", [id]);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
async
|
|
118
|
-
await this.db.run("DELETE FROM packages WHERE
|
|
117
|
+
async deletePackagesByEnvironmentId(id: string): Promise<void> {
|
|
118
|
+
await this.db.run("DELETE FROM packages WHERE environment_id = ?", [id]);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
private mapToPackage(row: Record<string, unknown>): Package {
|
|
122
122
|
return {
|
|
123
123
|
id: row.id as string,
|
|
124
|
-
|
|
124
|
+
environmentId: row.environment_id as string,
|
|
125
125
|
name: row.name as string,
|
|
126
126
|
description: row.description as string | undefined,
|
|
127
127
|
manifestPath: row.manifest_path as string,
|
|
@@ -6,7 +6,7 @@ import { DuckDBManifestStore } from "./DuckDBManifestStore";
|
|
|
6
6
|
function makeEntry(overrides: Partial<ManifestEntry> = {}): ManifestEntry {
|
|
7
7
|
return {
|
|
8
8
|
id: "entry-1",
|
|
9
|
-
|
|
9
|
+
environmentId: "proj-1",
|
|
10
10
|
packageName: "pkg",
|
|
11
11
|
buildId: "build-abc",
|
|
12
12
|
tableName: "my_table",
|
|
@@ -83,7 +83,7 @@ describe("DuckDBManifestStore", () => {
|
|
|
83
83
|
expect(ctx.repository.upsertManifestEntry.calledOnce).toBe(true);
|
|
84
84
|
const arg = ctx.repository.upsertManifestEntry.firstCall.args[0];
|
|
85
85
|
expect(arg).toEqual({
|
|
86
|
-
|
|
86
|
+
environmentId: "proj-1",
|
|
87
87
|
packageName: "pkg",
|
|
88
88
|
buildId: "build-abc",
|
|
89
89
|
tableName: "tbl",
|
|
@@ -17,12 +17,21 @@ export async function initializeSchema(
|
|
|
17
17
|
);
|
|
18
18
|
await dropAllTables(db);
|
|
19
19
|
} else {
|
|
20
|
+
// TODO: Remove this during projects cleanup
|
|
21
|
+
// If a pre-rename `projects` schema is on disk, the new
|
|
22
|
+
// CREATE TABLE IF NOT EXISTS pass below would silently leave child
|
|
23
|
+
// tables on the old `project_id` column and the first query against
|
|
24
|
+
// `environment_id` would crash. Drop the legacy tables (with a loud
|
|
25
|
+
// warning) so the fresh schema can be created cleanly. This is
|
|
26
|
+
// destructive — operators upgrading should re-create their environments
|
|
27
|
+
// and packages via the API after the upgrade.
|
|
28
|
+
await dropLegacyProjectSchema(db);
|
|
20
29
|
logger.info("Creating database schema for the first time...");
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
//
|
|
32
|
+
// Environments table
|
|
24
33
|
await db.run(`
|
|
25
|
-
CREATE TABLE IF NOT EXISTS
|
|
34
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
26
35
|
id VARCHAR PRIMARY KEY,
|
|
27
36
|
name VARCHAR NOT NULL UNIQUE,
|
|
28
37
|
path VARCHAR NOT NULL,
|
|
@@ -37,15 +46,15 @@ export async function initializeSchema(
|
|
|
37
46
|
await db.run(`
|
|
38
47
|
CREATE TABLE IF NOT EXISTS packages (
|
|
39
48
|
id VARCHAR PRIMARY KEY,
|
|
40
|
-
|
|
49
|
+
environment_id VARCHAR NOT NULL,
|
|
41
50
|
name VARCHAR NOT NULL,
|
|
42
51
|
description VARCHAR,
|
|
43
52
|
manifest_path VARCHAR NOT NULL,
|
|
44
53
|
metadata JSON,
|
|
45
54
|
created_at TIMESTAMP NOT NULL,
|
|
46
55
|
updated_at TIMESTAMP NOT NULL,
|
|
47
|
-
FOREIGN KEY (
|
|
48
|
-
UNIQUE (
|
|
56
|
+
FOREIGN KEY (environment_id) REFERENCES environments(id),
|
|
57
|
+
UNIQUE (environment_id, name)
|
|
49
58
|
)
|
|
50
59
|
`);
|
|
51
60
|
|
|
@@ -53,22 +62,22 @@ export async function initializeSchema(
|
|
|
53
62
|
await db.run(`
|
|
54
63
|
CREATE TABLE IF NOT EXISTS connections (
|
|
55
64
|
id VARCHAR PRIMARY KEY,
|
|
56
|
-
|
|
65
|
+
environment_id VARCHAR NOT NULL,
|
|
57
66
|
name VARCHAR NOT NULL,
|
|
58
67
|
type VARCHAR NOT NULL,
|
|
59
68
|
config JSON NOT NULL,
|
|
60
69
|
created_at TIMESTAMP NOT NULL,
|
|
61
70
|
updated_at TIMESTAMP NOT NULL,
|
|
62
|
-
FOREIGN KEY (
|
|
63
|
-
UNIQUE (
|
|
71
|
+
FOREIGN KEY (environment_id) REFERENCES environments(id),
|
|
72
|
+
UNIQUE (environment_id, name)
|
|
64
73
|
)
|
|
65
74
|
`);
|
|
66
75
|
|
|
67
76
|
// Materializations table.
|
|
68
77
|
//
|
|
69
78
|
// `active_key` enforces at-most-one active (PENDING or RUNNING)
|
|
70
|
-
// materialization per (
|
|
71
|
-
// `{
|
|
79
|
+
// materialization per (environment, package) at the DB layer. It is set to
|
|
80
|
+
// `{environment_id}|{package_name}` while the row is active and cleared
|
|
72
81
|
// to NULL on transition to any terminal state. A unique index on
|
|
73
82
|
// `active_key` (see below) makes the insert-then-check race impossible —
|
|
74
83
|
// a second concurrent create fails with a constraint violation, which the
|
|
@@ -76,7 +85,7 @@ export async function initializeSchema(
|
|
|
76
85
|
await db.run(`
|
|
77
86
|
CREATE TABLE IF NOT EXISTS materializations (
|
|
78
87
|
id VARCHAR PRIMARY KEY,
|
|
79
|
-
|
|
88
|
+
environment_id VARCHAR NOT NULL,
|
|
80
89
|
package_name VARCHAR NOT NULL,
|
|
81
90
|
status VARCHAR NOT NULL,
|
|
82
91
|
active_key VARCHAR,
|
|
@@ -86,7 +95,7 @@ export async function initializeSchema(
|
|
|
86
95
|
metadata JSON,
|
|
87
96
|
created_at TIMESTAMP NOT NULL,
|
|
88
97
|
updated_at TIMESTAMP NOT NULL,
|
|
89
|
-
FOREIGN KEY (
|
|
98
|
+
FOREIGN KEY (environment_id) REFERENCES environments(id)
|
|
90
99
|
)
|
|
91
100
|
`);
|
|
92
101
|
|
|
@@ -94,7 +103,7 @@ export async function initializeSchema(
|
|
|
94
103
|
await db.run(`
|
|
95
104
|
CREATE TABLE IF NOT EXISTS build_manifests (
|
|
96
105
|
id VARCHAR PRIMARY KEY,
|
|
97
|
-
|
|
106
|
+
environment_id VARCHAR NOT NULL,
|
|
98
107
|
package_name VARCHAR NOT NULL,
|
|
99
108
|
build_id VARCHAR NOT NULL,
|
|
100
109
|
table_name VARCHAR NOT NULL,
|
|
@@ -102,36 +111,68 @@ export async function initializeSchema(
|
|
|
102
111
|
connection_name VARCHAR NOT NULL,
|
|
103
112
|
created_at TIMESTAMP NOT NULL,
|
|
104
113
|
updated_at TIMESTAMP NOT NULL,
|
|
105
|
-
FOREIGN KEY (
|
|
106
|
-
UNIQUE (
|
|
114
|
+
FOREIGN KEY (environment_id) REFERENCES environments(id),
|
|
115
|
+
UNIQUE (environment_id, package_name, build_id)
|
|
107
116
|
)
|
|
108
117
|
`);
|
|
109
118
|
|
|
110
119
|
// Create indexes for better query performance
|
|
111
120
|
await db.run(
|
|
112
|
-
"CREATE INDEX IF NOT EXISTS
|
|
121
|
+
"CREATE INDEX IF NOT EXISTS idx_packages_environment_id ON packages(environment_id)",
|
|
113
122
|
);
|
|
114
123
|
await db.run(
|
|
115
|
-
"CREATE INDEX IF NOT EXISTS
|
|
124
|
+
"CREATE INDEX IF NOT EXISTS idx_connections_environment_id ON connections(environment_id)",
|
|
116
125
|
);
|
|
117
126
|
await db.run(
|
|
118
|
-
"CREATE INDEX IF NOT EXISTS
|
|
127
|
+
"CREATE INDEX IF NOT EXISTS idx_materializations_environment_package ON materializations(environment_id, package_name)",
|
|
119
128
|
);
|
|
120
129
|
await db.run(
|
|
121
130
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_materializations_active_key ON materializations(active_key)",
|
|
122
131
|
);
|
|
123
132
|
await db.run(
|
|
124
|
-
"CREATE INDEX IF NOT EXISTS
|
|
133
|
+
"CREATE INDEX IF NOT EXISTS idx_build_manifests_environment_package ON build_manifests(environment_id, package_name)",
|
|
125
134
|
);
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
// TODO: Remove this during projects cleanup
|
|
138
|
+
// Tables in the pre-rename schema, listed children-first so DROP order
|
|
139
|
+
// satisfies foreign-key dependencies on the legacy `projects` table.
|
|
140
|
+
const LEGACY_TABLES_DROP_ORDER = [
|
|
141
|
+
"build_manifests",
|
|
142
|
+
"materializations",
|
|
143
|
+
"packages",
|
|
144
|
+
"connections",
|
|
145
|
+
"projects",
|
|
146
|
+
] as const;
|
|
147
|
+
|
|
148
|
+
async function dropLegacyProjectSchema(db: DuckDBConnection): Promise<void> {
|
|
149
|
+
const legacy = await db.all<{ name: string }>(
|
|
150
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='projects'",
|
|
151
|
+
);
|
|
152
|
+
if (!legacy || legacy.length === 0) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
logger.warn(
|
|
157
|
+
"Detected legacy 'projects' schema. Dropping legacy tables; existing environments/packages/connections/materializations data will be lost. Re-create them via the API after upgrade.",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
for (const table of LEGACY_TABLES_DROP_ORDER) {
|
|
161
|
+
try {
|
|
162
|
+
await db.run(`DROP TABLE IF EXISTS ${table}`);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logger.warn(`Failed to drop legacy table ${table}:`, err);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
128
169
|
async function dropAllTables(db: DuckDBConnection): Promise<void> {
|
|
129
170
|
const tables = [
|
|
130
171
|
"build_manifests",
|
|
131
172
|
"materializations",
|
|
132
173
|
"packages",
|
|
133
174
|
"connections",
|
|
134
|
-
"
|
|
175
|
+
"environments",
|
|
135
176
|
];
|
|
136
177
|
|
|
137
178
|
logger.info("Dropping tables:", tables.join(", "));
|
|
@@ -31,23 +31,32 @@ import { DuckDBConnection } from "../duckdb/DuckDBConnection";
|
|
|
31
31
|
*/
|
|
32
32
|
export class DuckLakeManifestStore implements ManifestStore {
|
|
33
33
|
private readonly table: string;
|
|
34
|
+
private readonly environmentName: string;
|
|
34
35
|
|
|
35
36
|
constructor(
|
|
36
37
|
private db: DuckDBConnection,
|
|
37
38
|
catalogName: string,
|
|
39
|
+
environmentName: string,
|
|
38
40
|
) {
|
|
39
41
|
this.table = `${catalogName}.build_manifests`;
|
|
42
|
+
this.environmentName = environmentName;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* Idempotently creates the `build_manifests` table and indices in the
|
|
44
47
|
* DuckLake catalog. Safe to call from every worker on startup.
|
|
48
|
+
*
|
|
49
|
+
* Note: this table uses `project_name` for the partition column, unlike
|
|
50
|
+
* the local DuckDB `build_manifests` table (in `storage/duckdb/schema.ts`)
|
|
51
|
+
* which uses `project_id` and FK-references `projects.id`. The two stores
|
|
52
|
+
* partition by different identifiers — local random id vs cross-pod-stable
|
|
53
|
+
* project name — so they intentionally diverge here.
|
|
45
54
|
*/
|
|
46
55
|
async bootstrapSchema(): Promise<void> {
|
|
47
56
|
await this.db.run(`
|
|
48
57
|
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
49
58
|
id VARCHAR,
|
|
50
|
-
|
|
59
|
+
environment_name VARCHAR NOT NULL,
|
|
51
60
|
package_name VARCHAR NOT NULL,
|
|
52
61
|
build_id VARCHAR NOT NULL,
|
|
53
62
|
table_name VARCHAR NOT NULL,
|
|
@@ -61,12 +70,12 @@ export class DuckLakeManifestStore implements ManifestStore {
|
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
async getManifest(
|
|
64
|
-
|
|
73
|
+
_environmentId: string,
|
|
65
74
|
packageName: string,
|
|
66
75
|
): Promise<BuildManifest> {
|
|
67
76
|
const rows = await this.db.all<Record<string, unknown>>(
|
|
68
|
-
`SELECT * FROM ${this.table} WHERE
|
|
69
|
-
[
|
|
77
|
+
`SELECT * FROM ${this.table} WHERE environment_name = ? AND package_name = ? ORDER BY created_at DESC`,
|
|
78
|
+
[this.environmentName, packageName],
|
|
70
79
|
);
|
|
71
80
|
const manifest: BuildManifest = { entries: {}, strict: false };
|
|
72
81
|
for (const row of rows) {
|
|
@@ -88,7 +97,7 @@ export class DuckLakeManifestStore implements ManifestStore {
|
|
|
88
97
|
* {@link getManifest} deduplicates by build_id keeping the newest row.
|
|
89
98
|
*/
|
|
90
99
|
async writeEntry(
|
|
91
|
-
|
|
100
|
+
_environmentId: string,
|
|
92
101
|
packageName: string,
|
|
93
102
|
buildId: string,
|
|
94
103
|
tableName: string,
|
|
@@ -99,11 +108,11 @@ export class DuckLakeManifestStore implements ManifestStore {
|
|
|
99
108
|
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
100
109
|
|
|
101
110
|
await this.db.run(
|
|
102
|
-
`INSERT INTO ${this.table} (id,
|
|
111
|
+
`INSERT INTO ${this.table} (id, environment_name, package_name, build_id, table_name, source_name, connection_name, created_at, updated_at)
|
|
103
112
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
104
113
|
[
|
|
105
114
|
id,
|
|
106
|
-
|
|
115
|
+
this.environmentName,
|
|
107
116
|
packageName,
|
|
108
117
|
buildId,
|
|
109
118
|
tableName,
|
|
@@ -120,12 +129,12 @@ export class DuckLakeManifestStore implements ManifestStore {
|
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
async listEntries(
|
|
123
|
-
|
|
132
|
+
_environmentId: string,
|
|
124
133
|
packageName: string,
|
|
125
134
|
): Promise<ManifestEntry[]> {
|
|
126
135
|
const rows = await this.db.all<Record<string, unknown>>(
|
|
127
|
-
`SELECT * FROM ${this.table} WHERE
|
|
128
|
-
[
|
|
136
|
+
`SELECT * FROM ${this.table} WHERE environment_name = ? AND package_name = ? ORDER BY created_at DESC`,
|
|
137
|
+
[this.environmentName, packageName],
|
|
129
138
|
);
|
|
130
139
|
return rows.map(this.mapToEntry);
|
|
131
140
|
}
|
|
@@ -133,7 +142,7 @@ export class DuckLakeManifestStore implements ManifestStore {
|
|
|
133
142
|
private mapToEntry(row: Record<string, unknown>): ManifestEntry {
|
|
134
143
|
return {
|
|
135
144
|
id: row.id as string,
|
|
136
|
-
|
|
145
|
+
environmentId: row.environment_name as string,
|
|
137
146
|
packageName: row.package_name as string,
|
|
138
147
|
buildId: row.build_id as string,
|
|
139
148
|
tableName: row.table_name as string,
|
package/tests/harness/e2e.ts
CHANGED
|
@@ -28,7 +28,7 @@ let originalServerRoot: string | undefined;
|
|
|
28
28
|
*/
|
|
29
29
|
export async function startE2E(): Promise<E2EEnv & { stop(): Promise<void> }> {
|
|
30
30
|
//--------------------------------------------------------------------------
|
|
31
|
-
// 1. Set SERVER_ROOT so
|
|
31
|
+
// 1. Set SERVER_ROOT so EnvironmentStore loader finds publisher.config.json
|
|
32
32
|
//--------------------------------------------------------------------------
|
|
33
33
|
originalServerRoot = process.env.SERVER_ROOT;
|
|
34
34
|
// Use import.meta.url for cross-platform compatibility (works on Windows)
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Request,
|
|
6
6
|
Result,
|
|
7
7
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { Mutex } from "async-mutex";
|
|
8
9
|
import http from "http";
|
|
9
10
|
import { AddressInfo } from "net";
|
|
10
11
|
import path from "path";
|
|
@@ -24,42 +25,29 @@ export interface McpE2ETestEnvironment {
|
|
|
24
25
|
originalInitializeStorage: string | undefined;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
// Counter for unique port assignment per test suite
|
|
28
|
+
// Counter for unique port assignment per test suite (incremented only under e2eSetupMutex)
|
|
28
29
|
let portCounter = 0;
|
|
29
30
|
|
|
30
|
-
//
|
|
31
|
-
|
|
31
|
+
// True mutex: the previous Promise-based lock had a TOCTOU race where two beforeAll hooks
|
|
32
|
+
// could both pass the `if (initializationLock)` check and run setup concurrently, sharing
|
|
33
|
+
// one publisher.db / server singleton and causing flaky listResources and port collisions.
|
|
34
|
+
const e2eSetupMutex = new Mutex();
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Starts the real application server and connects a real MCP client.
|
|
35
38
|
*/
|
|
36
39
|
export async function setupE2ETestEnvironment(): Promise<McpE2ETestEnvironment> {
|
|
37
|
-
|
|
38
|
-
if (initializationLock) {
|
|
40
|
+
return e2eSetupMutex.runExclusive(async () => {
|
|
39
41
|
console.log(
|
|
40
|
-
"[E2E Test Setup]
|
|
42
|
+
"[E2E Test Setup] Acquired setup mutex; starting environment...",
|
|
41
43
|
);
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Create a new lock for this initialization
|
|
46
|
-
let resolveLock: () => void;
|
|
47
|
-
initializationLock = new Promise((resolve) => {
|
|
48
|
-
resolveLock = resolve;
|
|
44
|
+
return setupE2ETestEnvironmentInternal();
|
|
49
45
|
});
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
return await setupE2ETestEnvironmentInternal();
|
|
53
|
-
} finally {
|
|
54
|
-
// Release the lock
|
|
55
|
-
resolveLock!();
|
|
56
|
-
initializationLock = null;
|
|
57
|
-
}
|
|
58
46
|
}
|
|
59
47
|
|
|
60
48
|
async function setupE2ETestEnvironmentInternal(): Promise<McpE2ETestEnvironment> {
|
|
61
49
|
// --- Store and Set SERVER_ROOT Env Var ---
|
|
62
|
-
// The
|
|
50
|
+
// The EnvironmentStore relies on SERVER_ROOT to find publisher.config.json.
|
|
63
51
|
const originalServerRoot = process.env.SERVER_ROOT; // Store original value
|
|
64
52
|
// Resolve the path to 'packages/server' based on the location of this file
|
|
65
53
|
// Use import.meta.url for cross-platform compatibility (works on Windows)
|
|
@@ -129,9 +117,9 @@ async function setupE2ETestEnvironmentInternal(): Promise<McpE2ETestEnvironment>
|
|
|
129
117
|
|
|
130
118
|
// --- Wait for server to be ready (packages downloaded) ---
|
|
131
119
|
// Poll the readiness endpoint to ensure initialization completes before tests run
|
|
132
|
-
//
|
|
120
|
+
// Allow >90s: cold CI can spend most of the budget on clone + package load before markReady().
|
|
133
121
|
console.log("[E2E Test Setup] Waiting for server to be ready...");
|
|
134
|
-
const maxWaitTime =
|
|
122
|
+
const maxWaitTime = 150000; // 150 seconds (INITIALIZE_STORAGE + large samples can be slow)
|
|
135
123
|
const pollInterval = 1000; // Check every second
|
|
136
124
|
const startTime = Date.now();
|
|
137
125
|
let isReady = false;
|
package/tests/harness/mocks.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import sinon from "sinon";
|
|
2
|
-
import {
|
|
2
|
+
import { EnvironmentStore } from "../../src/service/environment_store";
|
|
3
3
|
|
|
4
|
-
/** Return a stubbed
|
|
5
|
-
export function
|
|
6
|
-
const
|
|
7
|
-
// For now just have
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
/** Return a stubbed EnvironmentStore where every lookup throws or returns minimal objects. */
|
|
5
|
+
export function fakeEnvironmentStore(): sinon.SinonStubbedInstance<EnvironmentStore> {
|
|
6
|
+
const es = sinon.createStubInstance(EnvironmentStore);
|
|
7
|
+
// For now just have getEnvironment reject; suites can stub more.
|
|
8
|
+
es.getEnvironment.rejects(
|
|
9
|
+
new Error("fakeEnvironmentStore: getEnvironment not stubbed"),
|
|
10
|
+
);
|
|
11
|
+
return es;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
// The runtime implementation of `McpServer` lives in the MCP SDK package, which
|
|
@@ -27,7 +29,7 @@ export type McpServer = DummyMcpServer;
|
|
|
27
29
|
/** Convenience helper mimicking the old mocks used in integration specs. */
|
|
28
30
|
export function createMalloyServiceMocks() {
|
|
29
31
|
return {
|
|
30
|
-
|
|
32
|
+
environmentStore: fakeEnvironmentStore(),
|
|
31
33
|
} as const;
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -12,8 +12,8 @@ export interface RestE2EEnv {
|
|
|
12
12
|
* reuses the cached Express app and binds on an OS-assigned port
|
|
13
13
|
* to avoid collisions.
|
|
14
14
|
*
|
|
15
|
-
* Callers are responsible for creating any test-specific
|
|
16
|
-
* via the REST API (POST /api/v0/
|
|
15
|
+
* Callers are responsible for creating any test-specific environments
|
|
16
|
+
* via the REST API (POST /api/v0/environments) and cleaning them up.
|
|
17
17
|
*/
|
|
18
18
|
export async function startRestE2E(): Promise<
|
|
19
19
|
RestE2EEnv & { stop(): Promise<void> }
|