@malloy-publisher/server 0.0.187 → 0.0.189-dev
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 +423 -60
- package/dist/app/assets/HomePage-Dn3E4CuB.js +1 -0
- package/dist/app/assets/{MainPage-B-RVib7-.js → MainPage-BzB3yoqi.js} +1 -1
- package/dist/app/assets/{ModelPage-Cv7TxfHc.js → ModelPage-C9O_sAXT.js} +1 -1
- package/dist/app/assets/PackagePage-DcxKEjBX.js +1 -0
- package/dist/app/assets/ProjectPage-BDj307rF.js +1 -0
- package/dist/app/assets/{RouteError-B7433Znd.js → RouteError-DAShbVCG.js} +1 -1
- package/dist/app/assets/{WorkbookPage-BBMaiWJS.js → WorkbookPage-Cs_XYEaB.js} +1 -1
- package/dist/app/assets/core-CjeTkq8O.es-BqRc6yhC.js +148 -0
- package/dist/app/assets/engine-oniguruma-C4vnmooL.es-jdkXmgTr.js +1 -0
- package/dist/app/assets/github-light-JYsPkUQd.es-DAi9KRSo.js +1 -0
- package/dist/app/assets/index-15BOvhp0.js +456 -0
- package/dist/app/assets/{index-CpOUcMUD.js → index-Bb2jqquW.js} +1 -1
- package/dist/app/assets/{index-DfdCzzKW.js → index-D68X76-7.js} +1 -1
- package/dist/app/assets/{index.umd-CAdcmnBE.js → index.umd-DGBekgSu.js} +1 -1
- package/dist/app/assets/json-71t8ZF9g.es-BQoSv7ci.js +1 -0
- package/dist/app/assets/sql-DCkt643-.es-COK4E0Yg.js +1 -0
- package/dist/app/assets/typescript-buWNZFwO.es-Dj6nwHGl.js +1 -0
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.js +98 -77
- package/dist/server.js +1834 -450
- package/package.json +5 -3
- package/src/controller/connection.controller.ts +27 -20
- package/src/controller/manifest.controller.ts +29 -0
- package/src/controller/materialization.controller.ts +125 -0
- package/src/controller/model.controller.ts +0 -2
- package/src/controller/package.controller.ts +53 -2
- package/src/errors.ts +24 -0
- package/src/server.ts +196 -5
- package/src/service/manifest_service.spec.ts +201 -0
- package/src/service/manifest_service.ts +106 -0
- package/src/service/materialization_service.spec.ts +648 -0
- package/src/service/materialization_service.ts +929 -0
- package/src/service/materialized_table_gc.spec.ts +383 -0
- package/src/service/materialized_table_gc.ts +279 -0
- package/src/service/model.ts +25 -4
- package/src/service/package.ts +50 -0
- package/src/service/project_store.ts +21 -2
- package/src/service/quoting.ts +41 -0
- package/src/service/resolve_project.ts +13 -0
- package/src/storage/DatabaseInterface.ts +103 -1
- package/src/storage/{StorageManager.spec.ts → StorageManager.mock.ts} +9 -0
- package/src/storage/StorageManager.ts +119 -1
- package/src/storage/duckdb/DuckDBManifestStore.ts +70 -0
- package/src/storage/duckdb/DuckDBRepository.ts +99 -9
- package/src/storage/duckdb/ManifestRepository.ts +119 -0
- package/src/storage/duckdb/MaterializationRepository.ts +249 -0
- package/src/storage/duckdb/manifest_store.spec.ts +133 -0
- package/src/storage/duckdb/schema.ts +59 -1
- package/src/storage/ducklake/DuckLakeManifestStore.ts +146 -0
- package/tests/fixtures/persist-test/data/orders.csv +5 -0
- package/tests/fixtures/persist-test/persist_test.malloy +11 -0
- package/tests/fixtures/persist-test/publisher.json +5 -0
- package/tests/fixtures/publisher.config.json +15 -0
- package/tests/harness/rest_e2e.ts +68 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +470 -0
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +2 -2
- package/dist/app/assets/HomePage-qyt2wz79.js +0 -1
- package/dist/app/assets/PackagePage-BOnk1rJb.js +0 -1
- package/dist/app/assets/ProjectPage-DFn9Ek1J.js +0 -1
- package/dist/app/assets/index-DjdBB2D6.js +0 -467
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/server",
|
|
3
3
|
"description": "Malloy Publisher Server",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.189-dev",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"malloy-publisher": "dist/server.js"
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "bun test
|
|
17
|
+
"test": "bun run test:unit && bun run test:integration",
|
|
18
|
+
"test:unit": "bun test --timeout 100000 src",
|
|
19
|
+
"test:integration": "bun test --timeout 100000 tests",
|
|
18
20
|
"build": "bun generate-api-types && bun build:app && NODE_ENV=production bun run build.ts",
|
|
19
21
|
"build:server-only": "bun generate-api-types && NODE_ENV=production bun run build.ts",
|
|
20
22
|
"start": "NODE_ENV=production node ./dist/server.js",
|
|
@@ -56,7 +58,7 @@
|
|
|
56
58
|
"class-transformer": "^0.5.1",
|
|
57
59
|
"class-validator": "^0.14.1",
|
|
58
60
|
"cors": "^2.8.5",
|
|
59
|
-
"duckdb": "1.4.
|
|
61
|
+
"duckdb": "1.4.4",
|
|
60
62
|
"express": "^4.21.0",
|
|
61
63
|
"globals": "^15.9.0",
|
|
62
64
|
"handlebars": "^4.7.8",
|
|
@@ -141,7 +141,10 @@ export class ConnectionController {
|
|
|
141
141
|
if (!source) {
|
|
142
142
|
throw new ConnectionError(`Table ${tablePath} not found`);
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
// BigQueryConnection returns `error.message` as a string on failure instead of throwing.
|
|
145
|
+
if (typeof source === "string") {
|
|
146
|
+
throw new ConnectionError(source);
|
|
147
|
+
}
|
|
145
148
|
return {
|
|
146
149
|
source: JSON.stringify(source),
|
|
147
150
|
resource: tablePath,
|
|
@@ -173,11 +176,11 @@ export class ConnectionController {
|
|
|
173
176
|
if (!projectName || !connectionName) {
|
|
174
177
|
throw new BadRequestError("Connection payload is required");
|
|
175
178
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
180
|
-
return
|
|
179
|
+
// Prefer the in-memory API connection (which was materialized by the
|
|
180
|
+
// project on load and carries `attributes`). The DB row stores the
|
|
181
|
+
// raw config and FK columns, which aren't the ApiConnection shape.
|
|
182
|
+
const project = await this.projectStore.getProject(projectName, false);
|
|
183
|
+
return project.getApiConnection(connectionName);
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
public async listConnections(projectName: string): Promise<ApiConnection[]> {
|
|
@@ -231,22 +234,26 @@ export class ConnectionController {
|
|
|
231
234
|
projectName,
|
|
232
235
|
connectionName,
|
|
233
236
|
);
|
|
234
|
-
|
|
235
237
|
try {
|
|
238
|
+
const schema = await (
|
|
239
|
+
malloyConnection as Connection & {
|
|
240
|
+
fetchSelectSchema: (params: {
|
|
241
|
+
connection: string;
|
|
242
|
+
selectStr: string;
|
|
243
|
+
}) => Promise<unknown>;
|
|
244
|
+
}
|
|
245
|
+
).fetchSelectSchema({
|
|
246
|
+
connection: connectionName,
|
|
247
|
+
selectStr: sqlStatement,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// BigQueryConnection returns `error.message` as a string on failure instead of throwing.
|
|
251
|
+
if (typeof schema === "string") {
|
|
252
|
+
throw new ConnectionError(schema);
|
|
253
|
+
}
|
|
254
|
+
|
|
236
255
|
return {
|
|
237
|
-
source: JSON.stringify(
|
|
238
|
-
await (
|
|
239
|
-
malloyConnection as Connection & {
|
|
240
|
-
fetchSelectSchema: (params: {
|
|
241
|
-
connection: string;
|
|
242
|
-
selectStr: string;
|
|
243
|
-
}) => Promise<unknown>;
|
|
244
|
-
}
|
|
245
|
-
).fetchSelectSchema({
|
|
246
|
-
connection: connectionName,
|
|
247
|
-
selectStr: sqlStatement,
|
|
248
|
-
}),
|
|
249
|
-
),
|
|
256
|
+
source: JSON.stringify(schema),
|
|
250
257
|
};
|
|
251
258
|
} catch (error) {
|
|
252
259
|
throw new ConnectionError((error as Error).message);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ManifestService } from "../service/manifest_service";
|
|
2
|
+
import { ProjectStore } from "../service/project_store";
|
|
3
|
+
import { resolveProjectId } from "../service/resolve_project";
|
|
4
|
+
|
|
5
|
+
export class ManifestController {
|
|
6
|
+
constructor(
|
|
7
|
+
private projectStore: ProjectStore,
|
|
8
|
+
private manifestService: ManifestService,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
async getManifest(projectName: string, packageName: string) {
|
|
12
|
+
const repository = this.projectStore.storageManager.getRepository();
|
|
13
|
+
const projectId = await resolveProjectId(repository, projectName);
|
|
14
|
+
// Verify the package exists so we return 404 instead of an empty manifest.
|
|
15
|
+
const project = await this.projectStore.getProject(projectName, false);
|
|
16
|
+
await project.getPackage(packageName, false);
|
|
17
|
+
return this.manifestService.getManifest(projectId, packageName);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async reloadManifest(projectName: string, packageName: string) {
|
|
21
|
+
const repository = this.projectStore.storageManager.getRepository();
|
|
22
|
+
const projectId = await resolveProjectId(repository, projectName);
|
|
23
|
+
return this.manifestService.reloadManifest(
|
|
24
|
+
projectId,
|
|
25
|
+
packageName,
|
|
26
|
+
projectName,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { BadRequestError } from "../errors";
|
|
2
|
+
import { MaterializationService } from "../service/materialization_service";
|
|
3
|
+
|
|
4
|
+
export class MaterializationController {
|
|
5
|
+
constructor(private materializationService: MaterializationService) {}
|
|
6
|
+
|
|
7
|
+
async createMaterialization(
|
|
8
|
+
projectName: string,
|
|
9
|
+
packageName: string,
|
|
10
|
+
body: Record<string, unknown>,
|
|
11
|
+
) {
|
|
12
|
+
const options = this.validateCreateBody(body);
|
|
13
|
+
return this.materializationService.createMaterialization(
|
|
14
|
+
projectName,
|
|
15
|
+
packageName,
|
|
16
|
+
options,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private validateCreateBody(body: Record<string, unknown>): {
|
|
21
|
+
forceRefresh?: boolean;
|
|
22
|
+
autoLoadManifest?: boolean;
|
|
23
|
+
} {
|
|
24
|
+
const result: { forceRefresh?: boolean; autoLoadManifest?: boolean } = {};
|
|
25
|
+
if (body.forceRefresh !== undefined) {
|
|
26
|
+
if (typeof body.forceRefresh !== "boolean") {
|
|
27
|
+
throw new BadRequestError("forceRefresh must be a boolean");
|
|
28
|
+
}
|
|
29
|
+
result.forceRefresh = body.forceRefresh;
|
|
30
|
+
}
|
|
31
|
+
if (body.autoLoadManifest !== undefined) {
|
|
32
|
+
if (typeof body.autoLoadManifest !== "boolean") {
|
|
33
|
+
throw new BadRequestError("autoLoadManifest must be a boolean");
|
|
34
|
+
}
|
|
35
|
+
result.autoLoadManifest = body.autoLoadManifest;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async startMaterialization(
|
|
41
|
+
projectName: string,
|
|
42
|
+
packageName: string,
|
|
43
|
+
materializationId: string,
|
|
44
|
+
) {
|
|
45
|
+
return this.materializationService.startMaterialization(
|
|
46
|
+
projectName,
|
|
47
|
+
packageName,
|
|
48
|
+
materializationId,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async stopMaterialization(
|
|
53
|
+
projectName: string,
|
|
54
|
+
packageName: string,
|
|
55
|
+
materializationId: string,
|
|
56
|
+
) {
|
|
57
|
+
return this.materializationService.stopMaterialization(
|
|
58
|
+
projectName,
|
|
59
|
+
packageName,
|
|
60
|
+
materializationId,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async listMaterializations(
|
|
65
|
+
projectName: string,
|
|
66
|
+
packageName: string,
|
|
67
|
+
options?: { limit?: number; offset?: number },
|
|
68
|
+
) {
|
|
69
|
+
return this.materializationService.listMaterializations(
|
|
70
|
+
projectName,
|
|
71
|
+
packageName,
|
|
72
|
+
options,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getMaterialization(
|
|
77
|
+
projectName: string,
|
|
78
|
+
packageName: string,
|
|
79
|
+
materializationId: string,
|
|
80
|
+
) {
|
|
81
|
+
return this.materializationService.getMaterialization(
|
|
82
|
+
projectName,
|
|
83
|
+
packageName,
|
|
84
|
+
materializationId,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async deleteMaterialization(
|
|
89
|
+
projectName: string,
|
|
90
|
+
packageName: string,
|
|
91
|
+
materializationId: string,
|
|
92
|
+
) {
|
|
93
|
+
return this.materializationService.deleteMaterialization(
|
|
94
|
+
projectName,
|
|
95
|
+
packageName,
|
|
96
|
+
materializationId,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async teardownPackage(
|
|
101
|
+
projectName: string,
|
|
102
|
+
packageName: string,
|
|
103
|
+
body: Record<string, unknown>,
|
|
104
|
+
) {
|
|
105
|
+
const options = this.validateTeardownBody(body);
|
|
106
|
+
return this.materializationService.teardownPackage(
|
|
107
|
+
projectName,
|
|
108
|
+
packageName,
|
|
109
|
+
options,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private validateTeardownBody(body: Record<string, unknown>): {
|
|
114
|
+
dryRun?: boolean;
|
|
115
|
+
} {
|
|
116
|
+
const options: { dryRun?: boolean } = {};
|
|
117
|
+
if (body.dryRun !== undefined) {
|
|
118
|
+
if (typeof body.dryRun !== "boolean") {
|
|
119
|
+
throw new BadRequestError("dryRun must be a boolean");
|
|
120
|
+
}
|
|
121
|
+
options.dryRun = body.dryRun;
|
|
122
|
+
}
|
|
123
|
+
return options;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -7,8 +7,6 @@ type ApiNotebook = components["schemas"]["Notebook"];
|
|
|
7
7
|
type ApiModel = components["schemas"]["Model"];
|
|
8
8
|
type ApiCompiledModel = components["schemas"]["CompiledModel"];
|
|
9
9
|
type ApiRawNotebook = components["schemas"]["RawNotebook"];
|
|
10
|
-
export type ListModelsFilterEnum =
|
|
11
|
-
components["parameters"]["ListModelsFilterEnum"];
|
|
12
10
|
export class ModelController {
|
|
13
11
|
private projectStore: ProjectStore;
|
|
14
12
|
|
|
@@ -2,15 +2,19 @@ import * as path from "path";
|
|
|
2
2
|
import { components } from "../api";
|
|
3
3
|
import { PUBLISHER_DATA_DIR } from "../constants";
|
|
4
4
|
import { BadRequestError, FrozenConfigError } from "../errors";
|
|
5
|
+
import { logger } from "../logger";
|
|
6
|
+
import { ManifestService } from "../service/manifest_service";
|
|
5
7
|
import { ProjectStore } from "../service/project_store";
|
|
6
8
|
|
|
7
9
|
type ApiPackage = components["schemas"]["Package"];
|
|
8
10
|
|
|
9
11
|
export class PackageController {
|
|
10
12
|
private projectStore: ProjectStore;
|
|
13
|
+
private manifestService: ManifestService;
|
|
11
14
|
|
|
12
|
-
constructor(projectStore: ProjectStore) {
|
|
15
|
+
constructor(projectStore: ProjectStore, manifestService: ManifestService) {
|
|
13
16
|
this.projectStore = projectStore;
|
|
17
|
+
this.manifestService = manifestService;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
public async listPackages(projectName: string): Promise<ApiPackage[]> {
|
|
@@ -32,7 +36,11 @@ export class PackageController {
|
|
|
32
36
|
return _package.getPackageMetadata();
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
async addPackage(
|
|
39
|
+
async addPackage(
|
|
40
|
+
projectName: string,
|
|
41
|
+
body: ApiPackage,
|
|
42
|
+
options?: { autoLoadManifest?: boolean },
|
|
43
|
+
) {
|
|
36
44
|
if (this.projectStore.publisherConfigIsFrozen) {
|
|
37
45
|
throw new FrozenConfigError();
|
|
38
46
|
}
|
|
@@ -46,9 +54,52 @@ export class PackageController {
|
|
|
46
54
|
const result = await project.addPackage(body.name);
|
|
47
55
|
await this.projectStore.addPackageToDatabase(projectName, body.name);
|
|
48
56
|
|
|
57
|
+
if (options?.autoLoadManifest === true) {
|
|
58
|
+
await this.tryLoadExistingManifest(projectName, body.name);
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
return result;
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
/**
|
|
65
|
+
* If there are already manifest entries for this package (e.g. from a
|
|
66
|
+
* previous materialization run), reload all models with the manifest so
|
|
67
|
+
* persist references resolve to the materialized tables immediately.
|
|
68
|
+
*/
|
|
69
|
+
private async tryLoadExistingManifest(
|
|
70
|
+
projectName: string,
|
|
71
|
+
packageName: string,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
const repository = this.projectStore.storageManager.getRepository();
|
|
75
|
+
const dbProject = await repository.getProjectByName(projectName);
|
|
76
|
+
if (!dbProject) return;
|
|
77
|
+
|
|
78
|
+
const manifest = await this.manifestService.getManifest(
|
|
79
|
+
dbProject.id,
|
|
80
|
+
packageName,
|
|
81
|
+
);
|
|
82
|
+
if (Object.keys(manifest.entries).length === 0) return;
|
|
83
|
+
|
|
84
|
+
await this.manifestService.reloadManifest(
|
|
85
|
+
dbProject.id,
|
|
86
|
+
packageName,
|
|
87
|
+
projectName,
|
|
88
|
+
);
|
|
89
|
+
logger.info("Auto-loaded existing manifest for added package", {
|
|
90
|
+
projectName,
|
|
91
|
+
packageName,
|
|
92
|
+
entryCount: Object.keys(manifest.entries).length,
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger.warn("Failed to auto-load manifest for package", {
|
|
96
|
+
projectName,
|
|
97
|
+
packageName,
|
|
98
|
+
error,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
52
103
|
public async deletePackage(projectName: string, packageName: string) {
|
|
53
104
|
if (this.projectStore.publisherConfigIsFrozen) {
|
|
54
105
|
throw new FrozenConfigError();
|
package/src/errors.ts
CHANGED
|
@@ -20,6 +20,12 @@ export function internalErrorToHttpError(error: Error) {
|
|
|
20
20
|
return httpError(424, error.message);
|
|
21
21
|
} else if (error instanceof ConnectionError) {
|
|
22
22
|
return httpError(502, error.message);
|
|
23
|
+
} else if (error instanceof MaterializationNotFoundError) {
|
|
24
|
+
return httpError(404, error.message);
|
|
25
|
+
} else if (error instanceof MaterializationConflictError) {
|
|
26
|
+
return httpError(409, error.message);
|
|
27
|
+
} else if (error instanceof InvalidStateTransitionError) {
|
|
28
|
+
return httpError(409, error.message);
|
|
23
29
|
} else {
|
|
24
30
|
return httpError(500, error.message);
|
|
25
31
|
}
|
|
@@ -90,3 +96,21 @@ export class FrozenConfigError extends Error {
|
|
|
90
96
|
super(message);
|
|
91
97
|
}
|
|
92
98
|
}
|
|
99
|
+
|
|
100
|
+
export class MaterializationNotFoundError extends Error {
|
|
101
|
+
constructor(message: string) {
|
|
102
|
+
super(message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class MaterializationConflictError extends Error {
|
|
107
|
+
constructor(message: string) {
|
|
108
|
+
super(message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class InvalidStateTransitionError extends Error {
|
|
113
|
+
constructor(message: string) {
|
|
114
|
+
super(message);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -20,7 +20,11 @@ import { ModelController } from "./controller/model.controller";
|
|
|
20
20
|
import { PackageController } from "./controller/package.controller";
|
|
21
21
|
import { QueryController } from "./controller/query.controller";
|
|
22
22
|
import { WatchModeController } from "./controller/watch-mode.controller";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
BadRequestError,
|
|
25
|
+
internalErrorToHttpError,
|
|
26
|
+
NotImplementedError,
|
|
27
|
+
} from "./errors";
|
|
24
28
|
import {
|
|
25
29
|
drainingGuard,
|
|
26
30
|
registerHealthEndpoints,
|
|
@@ -28,7 +32,11 @@ import {
|
|
|
28
32
|
} from "./health";
|
|
29
33
|
import { logger, loggerMiddleware } from "./logger";
|
|
30
34
|
|
|
35
|
+
import { ManifestController } from "./controller/manifest.controller";
|
|
36
|
+
import { MaterializationController } from "./controller/materialization.controller";
|
|
31
37
|
import { initializeMcpServer } from "./mcp/server";
|
|
38
|
+
import { ManifestService } from "./service/manifest_service";
|
|
39
|
+
import { MaterializationService } from "./service/materialization_service";
|
|
32
40
|
import { ProjectStore } from "./service/project_store";
|
|
33
41
|
|
|
34
42
|
/** Normalize an Express query param into a string[] or undefined. */
|
|
@@ -125,17 +133,29 @@ const SERVER_ROOT = path.resolve(process.cwd(), process.env.SERVER_ROOT || ".");
|
|
|
125
133
|
const API_PREFIX = "/api/v0";
|
|
126
134
|
const isDevelopment = process.env["NODE_ENV"] === "development";
|
|
127
135
|
|
|
128
|
-
const app = express();
|
|
136
|
+
export const app = express();
|
|
129
137
|
app.use(loggerMiddleware);
|
|
130
138
|
app.use(httpMetricsMiddleware);
|
|
131
139
|
const projectStore = new ProjectStore(SERVER_ROOT);
|
|
140
|
+
const manifestService = new ManifestService(projectStore);
|
|
132
141
|
const watchModeController = new WatchModeController(projectStore);
|
|
133
142
|
const connectionController = new ConnectionController(projectStore);
|
|
134
143
|
const modelController = new ModelController(projectStore);
|
|
135
|
-
const packageController = new PackageController(projectStore);
|
|
144
|
+
const packageController = new PackageController(projectStore, manifestService);
|
|
136
145
|
const databaseController = new DatabaseController(projectStore);
|
|
137
146
|
const queryController = new QueryController(projectStore);
|
|
138
147
|
const compileController = new CompileController(projectStore);
|
|
148
|
+
const materializationService = new MaterializationService(
|
|
149
|
+
projectStore,
|
|
150
|
+
manifestService,
|
|
151
|
+
);
|
|
152
|
+
const materializationController = new MaterializationController(
|
|
153
|
+
materializationService,
|
|
154
|
+
);
|
|
155
|
+
const manifestController = new ManifestController(
|
|
156
|
+
projectStore,
|
|
157
|
+
manifestService,
|
|
158
|
+
);
|
|
139
159
|
|
|
140
160
|
export const mcpApp = express();
|
|
141
161
|
|
|
@@ -262,7 +282,9 @@ app.use(
|
|
|
262
282
|
credentials: true,
|
|
263
283
|
}),
|
|
264
284
|
);
|
|
265
|
-
|
|
285
|
+
|
|
286
|
+
// Set body-parser JSON limit to 1Mb (default: 100kb)
|
|
287
|
+
app.use(bodyParser.json({ limit: "1mb" }));
|
|
266
288
|
|
|
267
289
|
// Register health check endpoints on main app:
|
|
268
290
|
// - Required for production/Kubernetes monitoring (main server on PUBLISHER_PORT)
|
|
@@ -579,7 +601,7 @@ app.post(
|
|
|
579
601
|
req.params.projectName,
|
|
580
602
|
req.params.connectionName,
|
|
581
603
|
req.body.sqlStatement as string,
|
|
582
|
-
req.
|
|
604
|
+
req.body.options as string,
|
|
583
605
|
),
|
|
584
606
|
);
|
|
585
607
|
} catch (error) {
|
|
@@ -650,9 +672,11 @@ app.get(`${API_PREFIX}/projects/:projectName/packages`, async (req, res) => {
|
|
|
650
672
|
|
|
651
673
|
app.post(`${API_PREFIX}/projects/:projectName/packages`, async (req, res) => {
|
|
652
674
|
try {
|
|
675
|
+
const autoLoadManifest = req.query.autoLoadManifest === "true";
|
|
653
676
|
const _package = await packageController.addPackage(
|
|
654
677
|
req.params.projectName,
|
|
655
678
|
req.body,
|
|
679
|
+
{ autoLoadManifest },
|
|
656
680
|
);
|
|
657
681
|
res.status(200).json(_package?.getPackageMetadata());
|
|
658
682
|
} catch (error) {
|
|
@@ -953,6 +977,173 @@ app.post(
|
|
|
953
977
|
},
|
|
954
978
|
);
|
|
955
979
|
|
|
980
|
+
// ==================== MATERIALIZATION ROUTES ====================
|
|
981
|
+
|
|
982
|
+
app.post(
|
|
983
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/materializations`,
|
|
984
|
+
async (req, res) => {
|
|
985
|
+
try {
|
|
986
|
+
const build = await materializationController.createMaterialization(
|
|
987
|
+
req.params.projectName,
|
|
988
|
+
req.params.packageName,
|
|
989
|
+
req.body || {},
|
|
990
|
+
);
|
|
991
|
+
res.status(201).json(build);
|
|
992
|
+
} catch (error) {
|
|
993
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
994
|
+
res.status(status).json(json);
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
app.get(
|
|
1000
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/materializations`,
|
|
1001
|
+
async (req, res) => {
|
|
1002
|
+
try {
|
|
1003
|
+
const limit = req.query.limit
|
|
1004
|
+
? parseInt(req.query.limit as string, 10)
|
|
1005
|
+
: undefined;
|
|
1006
|
+
const offset = req.query.offset
|
|
1007
|
+
? parseInt(req.query.offset as string, 10)
|
|
1008
|
+
: undefined;
|
|
1009
|
+
const builds = await materializationController.listMaterializations(
|
|
1010
|
+
req.params.projectName,
|
|
1011
|
+
req.params.packageName,
|
|
1012
|
+
{ limit, offset },
|
|
1013
|
+
);
|
|
1014
|
+
res.status(200).json(builds);
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1017
|
+
res.status(status).json(json);
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
app.get(
|
|
1023
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
|
|
1024
|
+
async (req, res) => {
|
|
1025
|
+
try {
|
|
1026
|
+
const build = await materializationController.getMaterialization(
|
|
1027
|
+
req.params.projectName,
|
|
1028
|
+
req.params.packageName,
|
|
1029
|
+
req.params.materializationId,
|
|
1030
|
+
);
|
|
1031
|
+
res.status(200).json(build);
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1034
|
+
res.status(status).json(json);
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
app.post(
|
|
1040
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/teardown`,
|
|
1041
|
+
async (req, res) => {
|
|
1042
|
+
try {
|
|
1043
|
+
const result = await materializationController.teardownPackage(
|
|
1044
|
+
req.params.projectName,
|
|
1045
|
+
req.params.packageName,
|
|
1046
|
+
req.body || {},
|
|
1047
|
+
);
|
|
1048
|
+
res.status(200).json(result);
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1051
|
+
res.status(status).json(json);
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
app.post(
|
|
1057
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
|
|
1058
|
+
async (req, res) => {
|
|
1059
|
+
try {
|
|
1060
|
+
const action = req.query.action;
|
|
1061
|
+
if (action === "start") {
|
|
1062
|
+
const build = await materializationController.startMaterialization(
|
|
1063
|
+
req.params.projectName,
|
|
1064
|
+
req.params.packageName,
|
|
1065
|
+
req.params.materializationId,
|
|
1066
|
+
);
|
|
1067
|
+
res.status(202).json(build);
|
|
1068
|
+
} else if (action === "stop") {
|
|
1069
|
+
const build = await materializationController.stopMaterialization(
|
|
1070
|
+
req.params.projectName,
|
|
1071
|
+
req.params.packageName,
|
|
1072
|
+
req.params.materializationId,
|
|
1073
|
+
);
|
|
1074
|
+
res.status(200).json(build);
|
|
1075
|
+
} else {
|
|
1076
|
+
throw new BadRequestError(
|
|
1077
|
+
`Unsupported action '${String(action ?? "")}'. Expected 'start' or 'stop'.`,
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1082
|
+
res.status(status).json(json);
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
app.delete(
|
|
1088
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
|
|
1089
|
+
async (req, res) => {
|
|
1090
|
+
try {
|
|
1091
|
+
await materializationController.deleteMaterialization(
|
|
1092
|
+
req.params.projectName,
|
|
1093
|
+
req.params.packageName,
|
|
1094
|
+
req.params.materializationId,
|
|
1095
|
+
);
|
|
1096
|
+
res.status(204).send();
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1099
|
+
res.status(status).json(json);
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
// ==================== MANIFEST ROUTES ====================
|
|
1105
|
+
|
|
1106
|
+
app.get(
|
|
1107
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
|
|
1108
|
+
async (req, res) => {
|
|
1109
|
+
try {
|
|
1110
|
+
const manifest = await manifestController.getManifest(
|
|
1111
|
+
req.params.projectName,
|
|
1112
|
+
req.params.packageName,
|
|
1113
|
+
);
|
|
1114
|
+
res.status(200).json(manifest);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
logger.error("Get manifest error", { error });
|
|
1117
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1118
|
+
res.status(status).json(json);
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
app.post(
|
|
1124
|
+
`${API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
|
|
1125
|
+
async (req, res) => {
|
|
1126
|
+
try {
|
|
1127
|
+
const action = req.query.action;
|
|
1128
|
+
if (action === "reload") {
|
|
1129
|
+
const manifest = await manifestController.reloadManifest(
|
|
1130
|
+
req.params.projectName,
|
|
1131
|
+
req.params.packageName,
|
|
1132
|
+
);
|
|
1133
|
+
res.status(200).json(manifest);
|
|
1134
|
+
} else {
|
|
1135
|
+
throw new BadRequestError(
|
|
1136
|
+
`Unsupported action '${String(action ?? "")}'. Expected 'reload'.`,
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
logger.error("Manifest action error", { error });
|
|
1141
|
+
const { json, status } = internalErrorToHttpError(error as Error);
|
|
1142
|
+
res.status(status).json(json);
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
);
|
|
1146
|
+
|
|
956
1147
|
// Modify the catch-all route to only serve index.html in production
|
|
957
1148
|
if (!isDevelopment) {
|
|
958
1149
|
app.get("*", (_req, res) => res.sendFile(path.resolve(ROOT, "index.html")));
|