@malloy-publisher/server 0.0.196-dev → 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 +1954 -1318
- package/package.json +1 -1
- 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 -85
- package/src/service/{project_store.ts → environment_store.ts} +368 -326
- 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 +2 -2
- 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 +24 -23
- 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 +14 -14
- package/tests/fixtures/publisher.config.json +1 -1
- package/tests/harness/e2e.ts +1 -1
- package/tests/harness/mcp_test_setup.ts +1 -1
- 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 +27 -48
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +26 -35
- 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
|
@@ -11,7 +11,7 @@ import { components } from "../api";
|
|
|
11
11
|
import {
|
|
12
12
|
getProcessedPublisherConfig,
|
|
13
13
|
isPublisherConfigFrozen,
|
|
14
|
-
|
|
14
|
+
ProcessedEnvironment,
|
|
15
15
|
ProcessedPublisherConfig,
|
|
16
16
|
} from "../config";
|
|
17
17
|
import {
|
|
@@ -21,16 +21,16 @@ import {
|
|
|
21
21
|
} from "../constants";
|
|
22
22
|
import {
|
|
23
23
|
BadRequestError,
|
|
24
|
+
EnvironmentNotFoundError,
|
|
24
25
|
FrozenConfigError,
|
|
25
26
|
PackageNotFoundError,
|
|
26
|
-
ProjectNotFoundError,
|
|
27
27
|
} from "../errors";
|
|
28
28
|
import { getOperationalState, markNotReady, markReady } from "../health";
|
|
29
29
|
import { formatDuration, logger } from "../logger";
|
|
30
30
|
import { Connection } from "../storage/DatabaseInterface";
|
|
31
31
|
import { StorageConfig, StorageManager } from "../storage/StorageManager";
|
|
32
|
-
import {
|
|
33
|
-
type
|
|
32
|
+
import { Environment, PackageStatus } from "./environment";
|
|
33
|
+
type ApiEnvironment = components["schemas"]["Environment"];
|
|
34
34
|
|
|
35
35
|
const AZURE_SUPPORTED_SCHEMES = ["https://", "http://", "abfss://", "az://"];
|
|
36
36
|
const AZURE_DATA_EXTENSIONS = [
|
|
@@ -74,8 +74,8 @@ function validateAzureUrl(url: string, fieldName: string): void {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function
|
|
78
|
-
for (const conn of
|
|
77
|
+
function validateEnvironmentAzureUrls(environment: ApiEnvironment): void {
|
|
78
|
+
for (const conn of environment.connections || []) {
|
|
79
79
|
if (conn.type !== "duckdb") continue;
|
|
80
80
|
for (const db of conn.duckdbConnection?.attachedDatabases || []) {
|
|
81
81
|
if (db.type !== "azure" || !db.azureConnection) continue;
|
|
@@ -89,10 +89,10 @@ function validateProjectAzureUrls(project: ApiProject): void {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export class
|
|
92
|
+
export class EnvironmentStore {
|
|
93
93
|
public serverRootPath: string;
|
|
94
|
-
private
|
|
95
|
-
private
|
|
94
|
+
private environments: Map<string, Environment> = new Map();
|
|
95
|
+
private environmentMutexes = new Map<string, Mutex>();
|
|
96
96
|
public publisherConfigIsFrozen: boolean;
|
|
97
97
|
public finishedInitialization: Promise<void>;
|
|
98
98
|
private isInitialized: boolean = false;
|
|
@@ -117,29 +117,29 @@ export class ProjectStore {
|
|
|
117
117
|
this.finishedInitialization = this.initialize();
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
private async
|
|
120
|
+
private async addConfiguredEnvironment(environment: ProcessedEnvironment) {
|
|
121
121
|
try {
|
|
122
|
-
await this.
|
|
122
|
+
await this.addEnvironment(
|
|
123
123
|
{
|
|
124
|
-
name:
|
|
125
|
-
resource: `${API_PREFIX}/
|
|
126
|
-
connections:
|
|
127
|
-
packages:
|
|
124
|
+
name: environment.name,
|
|
125
|
+
resource: `${API_PREFIX}/environments/${environment.name}`,
|
|
126
|
+
connections: environment.connections,
|
|
127
|
+
packages: environment.packages,
|
|
128
128
|
},
|
|
129
129
|
true,
|
|
130
130
|
);
|
|
131
131
|
} catch (error) {
|
|
132
|
-
this.
|
|
132
|
+
this.logEnvironmentInitializationError(environment.name, error);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
private
|
|
137
|
-
|
|
136
|
+
private logEnvironmentInitializationError(
|
|
137
|
+
environmentName: string | undefined,
|
|
138
138
|
error: unknown,
|
|
139
139
|
) {
|
|
140
|
-
const label =
|
|
140
|
+
const label = environmentName ? ` "${environmentName}"` : "";
|
|
141
141
|
logger.error(
|
|
142
|
-
`Error initializing
|
|
142
|
+
`Error initializing environment${label}; skipping environment`,
|
|
143
143
|
this.extractErrorDataFromError(error),
|
|
144
144
|
);
|
|
145
145
|
}
|
|
@@ -155,62 +155,69 @@ export class ProjectStore {
|
|
|
155
155
|
this.serverRootPath,
|
|
156
156
|
);
|
|
157
157
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
const environmentManifest =
|
|
159
|
+
await EnvironmentStore.reloadEnvironmentManifest(
|
|
160
|
+
this.serverRootPath,
|
|
161
|
+
);
|
|
161
162
|
|
|
162
163
|
await this.cleanupAndCreatePublisherPath();
|
|
163
164
|
|
|
164
165
|
const repository = this.storageManager.getRepository();
|
|
165
166
|
|
|
166
167
|
if (reInit) {
|
|
167
|
-
// Load
|
|
168
|
+
// Load environments from config file
|
|
168
169
|
await Promise.all(
|
|
169
|
-
|
|
170
|
-
await this.
|
|
170
|
+
environmentManifest.environments.map(async (environment) => {
|
|
171
|
+
await this.addConfiguredEnvironment(environment);
|
|
171
172
|
}),
|
|
172
173
|
);
|
|
173
174
|
} else {
|
|
174
|
-
// Load existing
|
|
175
|
-
const
|
|
175
|
+
// Load existing environments from database
|
|
176
|
+
const existingEnvironments = await repository.listEnvironments();
|
|
176
177
|
|
|
177
|
-
if (
|
|
178
|
-
// Load
|
|
178
|
+
if (existingEnvironments.length > 0) {
|
|
179
|
+
// Load environments from database
|
|
179
180
|
await Promise.all(
|
|
180
|
-
|
|
181
|
+
existingEnvironments.map(async (dbEnvironment) => {
|
|
181
182
|
try {
|
|
182
|
-
// Check if
|
|
183
|
-
const
|
|
184
|
-
.access(
|
|
183
|
+
// Check if environment files exist on disk
|
|
184
|
+
const environmentExists = await fs.promises
|
|
185
|
+
.access(dbEnvironment.path)
|
|
185
186
|
.then(() => true)
|
|
186
187
|
.catch(() => false);
|
|
187
188
|
|
|
188
|
-
if (!
|
|
189
|
+
if (!environmentExists) {
|
|
189
190
|
// Try to find in config and reload
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
const environmentConfig =
|
|
192
|
+
environmentManifest.environments.find(
|
|
193
|
+
(p) => p.name === dbEnvironment.name,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (environmentConfig) {
|
|
197
|
+
const environmentInstance =
|
|
198
|
+
await this.addEnvironment(
|
|
199
|
+
{
|
|
200
|
+
name: environmentConfig.name,
|
|
201
|
+
resource: `${API_PREFIX}/environments/${environmentConfig.name}`,
|
|
202
|
+
connections:
|
|
203
|
+
environmentConfig.connections,
|
|
204
|
+
packages: environmentConfig.packages,
|
|
205
|
+
},
|
|
206
|
+
true,
|
|
207
|
+
);
|
|
193
208
|
|
|
194
|
-
|
|
195
|
-
|
|
209
|
+
// Update database with new path
|
|
210
|
+
await repository.updateEnvironment(
|
|
211
|
+
dbEnvironment.id,
|
|
196
212
|
{
|
|
197
|
-
|
|
198
|
-
resource: `${API_PREFIX}/projects/${projectConfig.name}`,
|
|
199
|
-
connections: projectConfig.connections,
|
|
200
|
-
packages: projectConfig.packages,
|
|
213
|
+
path: environmentInstance.metadata.location,
|
|
201
214
|
},
|
|
202
|
-
true,
|
|
203
215
|
);
|
|
204
216
|
|
|
205
|
-
|
|
206
|
-
await repository.updateProject(dbProject.id, {
|
|
207
|
-
path: projectInstance.metadata.location,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
return projectInstance.listPackages();
|
|
217
|
+
return environmentInstance.listPackages();
|
|
211
218
|
} else {
|
|
212
219
|
logger.error(
|
|
213
|
-
`
|
|
220
|
+
`Environment "${dbEnvironment.name}" not found in config and files missing`,
|
|
214
221
|
);
|
|
215
222
|
return;
|
|
216
223
|
}
|
|
@@ -218,12 +225,12 @@ export class ProjectStore {
|
|
|
218
225
|
|
|
219
226
|
// Get connections from database
|
|
220
227
|
const connections = await repository.listConnections(
|
|
221
|
-
|
|
228
|
+
dbEnvironment.id,
|
|
222
229
|
);
|
|
223
230
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
231
|
+
const environmentInstance = await Environment.create(
|
|
232
|
+
dbEnvironment.name,
|
|
233
|
+
dbEnvironment.path,
|
|
227
234
|
connections.map((conn) => ({
|
|
228
235
|
name: conn.name,
|
|
229
236
|
type: conn.type,
|
|
@@ -234,21 +241,24 @@ export class ProjectStore {
|
|
|
234
241
|
|
|
235
242
|
// Get packages from database
|
|
236
243
|
const packages = await repository.listPackages(
|
|
237
|
-
|
|
244
|
+
dbEnvironment.id,
|
|
238
245
|
);
|
|
239
246
|
packages.forEach((pkg) => {
|
|
240
|
-
|
|
247
|
+
environmentInstance.setPackageStatus(
|
|
241
248
|
pkg.name,
|
|
242
249
|
PackageStatus.SERVING,
|
|
243
250
|
);
|
|
244
251
|
});
|
|
245
252
|
|
|
246
|
-
this.
|
|
253
|
+
this.environments.set(
|
|
254
|
+
dbEnvironment.name,
|
|
255
|
+
environmentInstance,
|
|
256
|
+
);
|
|
247
257
|
|
|
248
|
-
return
|
|
258
|
+
return environmentInstance.listPackages();
|
|
249
259
|
} catch (error) {
|
|
250
|
-
this.
|
|
251
|
-
|
|
260
|
+
this.logEnvironmentInitializationError(
|
|
261
|
+
dbEnvironment.name,
|
|
252
262
|
error,
|
|
253
263
|
);
|
|
254
264
|
}
|
|
@@ -257,8 +267,8 @@ export class ProjectStore {
|
|
|
257
267
|
} else {
|
|
258
268
|
// Fallback to config file if database is empty
|
|
259
269
|
await Promise.all(
|
|
260
|
-
|
|
261
|
-
await this.
|
|
270
|
+
environmentManifest.environments.map(async (environment) => {
|
|
271
|
+
await this.addConfiguredEnvironment(environment);
|
|
262
272
|
}),
|
|
263
273
|
);
|
|
264
274
|
}
|
|
@@ -268,90 +278,99 @@ export class ProjectStore {
|
|
|
268
278
|
markReady();
|
|
269
279
|
const initializationDuration = performance.now() - initialTime;
|
|
270
280
|
logger.info(
|
|
271
|
-
`
|
|
281
|
+
`Environment store successfully initialized in ${formatDuration(initializationDuration)}`,
|
|
272
282
|
);
|
|
273
283
|
} catch (error) {
|
|
274
284
|
markNotReady();
|
|
275
285
|
const errorData = this.extractErrorDataFromError(error);
|
|
276
|
-
logger.error("Error initializing
|
|
286
|
+
logger.error("Error initializing environment store", errorData);
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
289
|
|
|
280
|
-
public async
|
|
281
|
-
|
|
282
|
-
|
|
290
|
+
public async addEnvironmentToDatabase(
|
|
291
|
+
environment: Environment,
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
if (!environment) {
|
|
294
|
+
logger.error("Cannot sync: environment is null or undefined");
|
|
283
295
|
return;
|
|
284
296
|
}
|
|
285
297
|
|
|
286
|
-
const
|
|
287
|
-
if (!
|
|
288
|
-
throw new Error("
|
|
298
|
+
const environmentName = environment.metadata?.name;
|
|
299
|
+
if (!environmentName) {
|
|
300
|
+
throw new Error("Environment name is required but not found");
|
|
289
301
|
}
|
|
290
302
|
|
|
291
303
|
const repository = this.storageManager.getRepository();
|
|
292
304
|
|
|
293
|
-
// Sync
|
|
294
|
-
const
|
|
305
|
+
// Sync environment metadata
|
|
306
|
+
const dbEnvironment = await this.addEnvironmentMetadata(
|
|
307
|
+
environment,
|
|
308
|
+
repository,
|
|
309
|
+
);
|
|
295
310
|
|
|
296
311
|
// Sync connections
|
|
297
|
-
await this.addConnections(
|
|
312
|
+
await this.addConnections(environment, dbEnvironment.id, repository);
|
|
298
313
|
|
|
299
314
|
// Sync packages
|
|
300
|
-
await this.addPackages(
|
|
315
|
+
await this.addPackages(environment, dbEnvironment.id, repository);
|
|
301
316
|
|
|
302
|
-
logger.info(`Synced
|
|
317
|
+
logger.info(`Synced environment "${environmentName}" to database`);
|
|
303
318
|
}
|
|
304
319
|
|
|
305
|
-
public async
|
|
320
|
+
public async deleteEnvironmentFromDatabase(
|
|
321
|
+
environmentName: string,
|
|
322
|
+
): Promise<void> {
|
|
306
323
|
const repository = this.storageManager.getRepository();
|
|
307
324
|
|
|
308
|
-
// Get the
|
|
309
|
-
const
|
|
325
|
+
// Get the environment from database
|
|
326
|
+
const dbEnvironment =
|
|
327
|
+
await repository.getEnvironmentByName(environmentName);
|
|
310
328
|
|
|
311
|
-
if (!
|
|
312
|
-
logger.error(`
|
|
329
|
+
if (!dbEnvironment) {
|
|
330
|
+
logger.error(`Environment "${environmentName}" not found in database`);
|
|
313
331
|
return;
|
|
314
332
|
}
|
|
315
333
|
|
|
316
|
-
// Delete the
|
|
317
|
-
await repository.
|
|
318
|
-
logger.info(`Deleted
|
|
334
|
+
// Delete the environment (this will cascade delete connections and packages)
|
|
335
|
+
await repository.deleteEnvironment(dbEnvironment.id);
|
|
336
|
+
logger.info(`Deleted environment "${environmentName}" from database`);
|
|
319
337
|
}
|
|
320
338
|
|
|
321
|
-
private async
|
|
322
|
-
|
|
339
|
+
private async addEnvironmentMetadata(
|
|
340
|
+
environment: Environment,
|
|
323
341
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
324
342
|
): Promise<{ id: string; name: string }> {
|
|
325
|
-
const
|
|
326
|
-
if (!
|
|
327
|
-
throw new Error("
|
|
328
|
-
}
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
name:
|
|
334
|
-
path:
|
|
335
|
-
description:
|
|
336
|
-
metadata:
|
|
343
|
+
const environmentName = environment.metadata?.name;
|
|
344
|
+
if (!environmentName) {
|
|
345
|
+
throw new Error("Environment name is required but not found");
|
|
346
|
+
}
|
|
347
|
+
const environmentPath = environment.metadata?.location || "";
|
|
348
|
+
const environmentDescription = environment.metadata?.readme;
|
|
349
|
+
|
|
350
|
+
const environmentData = {
|
|
351
|
+
name: environmentName,
|
|
352
|
+
path: environmentPath,
|
|
353
|
+
description: environmentDescription,
|
|
354
|
+
metadata: environment.metadata || {},
|
|
337
355
|
};
|
|
338
|
-
const
|
|
356
|
+
const existingEnvironment =
|
|
357
|
+
await repository.getEnvironmentByName(environmentName);
|
|
339
358
|
|
|
340
|
-
let
|
|
341
|
-
if (
|
|
359
|
+
let dbEnvironment: { id: string; name: string };
|
|
360
|
+
if (existingEnvironment) {
|
|
342
361
|
const updateData = {
|
|
343
|
-
description:
|
|
344
|
-
metadata:
|
|
362
|
+
description: environmentDescription,
|
|
363
|
+
metadata: environment.metadata || {},
|
|
345
364
|
};
|
|
346
365
|
|
|
347
|
-
await repository.
|
|
348
|
-
|
|
366
|
+
await repository.updateEnvironment(existingEnvironment.id, updateData);
|
|
367
|
+
dbEnvironment = { id: existingEnvironment.id, name: environmentName };
|
|
349
368
|
} else {
|
|
350
|
-
|
|
369
|
+
dbEnvironment = await repository.createEnvironment(environmentData);
|
|
351
370
|
}
|
|
352
371
|
|
|
353
|
-
// Initialize DuckLake manifest storage if configured on the
|
|
354
|
-
const materializationStorage =
|
|
372
|
+
// Initialize DuckLake manifest storage if configured on the environment.
|
|
373
|
+
const materializationStorage = environment.metadata
|
|
355
374
|
?.materializationStorage as
|
|
356
375
|
| { catalogUrl?: string; dataPath?: string }
|
|
357
376
|
| undefined;
|
|
@@ -359,9 +378,9 @@ export class ProjectStore {
|
|
|
359
378
|
materializationStorage?.catalogUrl &&
|
|
360
379
|
materializationStorage?.dataPath
|
|
361
380
|
) {
|
|
362
|
-
await this.storageManager.
|
|
363
|
-
|
|
364
|
-
|
|
381
|
+
await this.storageManager.initializeDuckLakeForEnvironment(
|
|
382
|
+
dbEnvironment.id,
|
|
383
|
+
dbEnvironment.name,
|
|
365
384
|
{
|
|
366
385
|
catalogUrl: materializationStorage.catalogUrl,
|
|
367
386
|
dataPath: materializationStorage.dataPath,
|
|
@@ -369,15 +388,15 @@ export class ProjectStore {
|
|
|
369
388
|
);
|
|
370
389
|
}
|
|
371
390
|
|
|
372
|
-
return
|
|
391
|
+
return dbEnvironment;
|
|
373
392
|
}
|
|
374
393
|
|
|
375
394
|
private async addPackages(
|
|
376
|
-
|
|
377
|
-
|
|
395
|
+
environment: Environment,
|
|
396
|
+
environmentId: string,
|
|
378
397
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
379
398
|
): Promise<void> {
|
|
380
|
-
const packages = await
|
|
399
|
+
const packages = await environment.listPackages();
|
|
381
400
|
|
|
382
401
|
// Sync each package
|
|
383
402
|
for (const pkg of packages) {
|
|
@@ -386,13 +405,13 @@ export class ProjectStore {
|
|
|
386
405
|
continue;
|
|
387
406
|
}
|
|
388
407
|
|
|
389
|
-
await this.addPackage(pkg,
|
|
408
|
+
await this.addPackage(pkg, environmentId, repository);
|
|
390
409
|
}
|
|
391
410
|
}
|
|
392
411
|
|
|
393
412
|
private async addPackage(
|
|
394
413
|
pkg: components["schemas"]["Package"],
|
|
395
|
-
|
|
414
|
+
environmentId: string,
|
|
396
415
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
397
416
|
): Promise<void> {
|
|
398
417
|
const pkgs = pkg as {
|
|
@@ -403,7 +422,7 @@ export class ProjectStore {
|
|
|
403
422
|
};
|
|
404
423
|
|
|
405
424
|
const packageData = {
|
|
406
|
-
|
|
425
|
+
environmentId,
|
|
407
426
|
name: pkgs.name,
|
|
408
427
|
description: pkgs.description ?? undefined,
|
|
409
428
|
manifestPath: pkgs.manifestPath ?? "",
|
|
@@ -421,7 +440,7 @@ export class ProjectStore {
|
|
|
421
440
|
) {
|
|
422
441
|
await this.updatePackage(
|
|
423
442
|
pkgs.name,
|
|
424
|
-
|
|
443
|
+
environmentId,
|
|
425
444
|
packageData,
|
|
426
445
|
repository,
|
|
427
446
|
);
|
|
@@ -433,7 +452,7 @@ export class ProjectStore {
|
|
|
433
452
|
|
|
434
453
|
private async updatePackage(
|
|
435
454
|
packageName: string,
|
|
436
|
-
|
|
455
|
+
environmentId: string,
|
|
437
456
|
packageData: {
|
|
438
457
|
description: string | undefined;
|
|
439
458
|
manifestPath: string;
|
|
@@ -442,7 +461,7 @@ export class ProjectStore {
|
|
|
442
461
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
443
462
|
): Promise<void> {
|
|
444
463
|
const existingPackage = await repository.getPackageByName(
|
|
445
|
-
|
|
464
|
+
environmentId,
|
|
446
465
|
packageName,
|
|
447
466
|
);
|
|
448
467
|
|
|
@@ -453,12 +472,12 @@ export class ProjectStore {
|
|
|
453
472
|
}
|
|
454
473
|
|
|
455
474
|
private async addConnections(
|
|
456
|
-
|
|
457
|
-
|
|
475
|
+
environment: Environment,
|
|
476
|
+
environmentId: string,
|
|
458
477
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
459
478
|
): Promise<void> {
|
|
460
479
|
try {
|
|
461
|
-
const connections =
|
|
480
|
+
const connections = environment.listApiConnections();
|
|
462
481
|
// Add/update connections
|
|
463
482
|
for (const conn of connections) {
|
|
464
483
|
if (!conn.name) {
|
|
@@ -468,26 +487,29 @@ export class ProjectStore {
|
|
|
468
487
|
|
|
469
488
|
// Check if connection exists
|
|
470
489
|
const existingConn = await repository.getConnectionByName(
|
|
471
|
-
|
|
490
|
+
environmentId,
|
|
472
491
|
conn.name,
|
|
473
492
|
);
|
|
474
493
|
|
|
475
494
|
if (existingConn) {
|
|
476
|
-
await this.updateConnection(conn,
|
|
495
|
+
await this.updateConnection(conn, environmentId, repository);
|
|
477
496
|
} else {
|
|
478
|
-
await this.addConnection(conn,
|
|
497
|
+
await this.addConnection(conn, environmentId, repository);
|
|
479
498
|
}
|
|
480
499
|
}
|
|
481
500
|
} catch (err: unknown) {
|
|
482
501
|
const error = err as Error;
|
|
483
|
-
const
|
|
484
|
-
logger.error(
|
|
502
|
+
const environmentName = environment.metadata?.name;
|
|
503
|
+
logger.error(
|
|
504
|
+
`Error syncing connections for "${environmentName}":`,
|
|
505
|
+
error,
|
|
506
|
+
);
|
|
485
507
|
}
|
|
486
508
|
}
|
|
487
509
|
|
|
488
510
|
public async addConnection(
|
|
489
|
-
conn: ReturnType<
|
|
490
|
-
|
|
511
|
+
conn: ReturnType<Environment["listApiConnections"]>[number],
|
|
512
|
+
environmentId: string,
|
|
491
513
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
492
514
|
): Promise<void> {
|
|
493
515
|
if (!conn.name) {
|
|
@@ -496,7 +518,7 @@ export class ProjectStore {
|
|
|
496
518
|
}
|
|
497
519
|
|
|
498
520
|
const connectionData = {
|
|
499
|
-
|
|
521
|
+
environmentId,
|
|
500
522
|
name: conn.name,
|
|
501
523
|
type: conn.type as Connection["type"],
|
|
502
524
|
config: conn,
|
|
@@ -513,8 +535,8 @@ export class ProjectStore {
|
|
|
513
535
|
}
|
|
514
536
|
|
|
515
537
|
public async updateConnection(
|
|
516
|
-
conn: ReturnType<
|
|
517
|
-
|
|
538
|
+
conn: ReturnType<Environment["listApiConnections"]>[number],
|
|
539
|
+
environmentId: string,
|
|
518
540
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
519
541
|
): Promise<void> {
|
|
520
542
|
if (!conn.name) {
|
|
@@ -522,12 +544,12 @@ export class ProjectStore {
|
|
|
522
544
|
}
|
|
523
545
|
|
|
524
546
|
const existingConn = await repository.getConnectionByName(
|
|
525
|
-
|
|
547
|
+
environmentId,
|
|
526
548
|
conn.name,
|
|
527
549
|
);
|
|
528
550
|
|
|
529
551
|
if (!existingConn) {
|
|
530
|
-
logger.error(`Connection "${conn.name}" not found in
|
|
552
|
+
logger.error(`Connection "${conn.name}" not found in environment`);
|
|
531
553
|
}
|
|
532
554
|
|
|
533
555
|
const connectionData = {
|
|
@@ -548,31 +570,34 @@ export class ProjectStore {
|
|
|
548
570
|
}
|
|
549
571
|
|
|
550
572
|
public async addPackageToDatabase(
|
|
551
|
-
|
|
573
|
+
environmentName: string,
|
|
552
574
|
packageName: string,
|
|
553
575
|
): Promise<void> {
|
|
554
|
-
const
|
|
576
|
+
const environment = await this.getEnvironment(environmentName, false);
|
|
555
577
|
const repository = this.storageManager.getRepository();
|
|
556
578
|
|
|
557
|
-
// Get the
|
|
558
|
-
const
|
|
579
|
+
// Get the environment ID from database
|
|
580
|
+
const dbEnvironment =
|
|
581
|
+
await repository.getEnvironmentByName(environmentName);
|
|
559
582
|
|
|
560
|
-
if (!
|
|
561
|
-
logger.error(`
|
|
562
|
-
throw new Error(
|
|
583
|
+
if (!dbEnvironment) {
|
|
584
|
+
logger.error(`Environment "${environmentName}" not found in database`);
|
|
585
|
+
throw new Error(
|
|
586
|
+
`Environment "${environmentName}" not found in database`,
|
|
587
|
+
);
|
|
563
588
|
}
|
|
564
589
|
|
|
565
|
-
// Get the package from the
|
|
566
|
-
const packages = await
|
|
590
|
+
// Get the package from the environment
|
|
591
|
+
const packages = await environment.listPackages();
|
|
567
592
|
const pkg = packages.find((p) => p.name === packageName);
|
|
568
593
|
|
|
569
594
|
if (!pkg) {
|
|
570
|
-
logger.warn(`Package "${packageName}" not found in
|
|
595
|
+
logger.warn(`Package "${packageName}" not found in environment`);
|
|
571
596
|
return;
|
|
572
597
|
}
|
|
573
598
|
|
|
574
599
|
// Sync the specific package
|
|
575
|
-
await this.addPackage(pkg,
|
|
600
|
+
await this.addPackage(pkg, dbEnvironment.id, repository);
|
|
576
601
|
logger.info(`Synced package "${packageName}" to database`);
|
|
577
602
|
}
|
|
578
603
|
|
|
@@ -580,22 +605,23 @@ export class ProjectStore {
|
|
|
580
605
|
* Delete a package from the database
|
|
581
606
|
*/
|
|
582
607
|
public async deletePackageFromDatabase(
|
|
583
|
-
|
|
608
|
+
environmentName: string,
|
|
584
609
|
packageName: string,
|
|
585
610
|
): Promise<void> {
|
|
586
611
|
const repository = this.storageManager.getRepository();
|
|
587
612
|
|
|
588
|
-
// Get the
|
|
589
|
-
const
|
|
613
|
+
// Get the environment ID from database
|
|
614
|
+
const dbEnvironment =
|
|
615
|
+
await repository.getEnvironmentByName(environmentName);
|
|
590
616
|
|
|
591
|
-
if (!
|
|
592
|
-
logger.error(`
|
|
617
|
+
if (!dbEnvironment) {
|
|
618
|
+
logger.error(`Environment "${environmentName}" not found in database`);
|
|
593
619
|
return;
|
|
594
620
|
}
|
|
595
621
|
|
|
596
622
|
// Find and delete the package
|
|
597
623
|
const existingPackage = await repository.getPackageByName(
|
|
598
|
-
|
|
624
|
+
dbEnvironment.id,
|
|
599
625
|
packageName,
|
|
600
626
|
);
|
|
601
627
|
|
|
@@ -644,13 +670,13 @@ export class ProjectStore {
|
|
|
644
670
|
await fs.promises.mkdir(uploadDocsPath, { recursive: true });
|
|
645
671
|
}
|
|
646
672
|
|
|
647
|
-
public async
|
|
673
|
+
public async listEnvironments(skipInitializationCheck: boolean = false) {
|
|
648
674
|
if (!skipInitializationCheck) {
|
|
649
675
|
await this.finishedInitialization;
|
|
650
676
|
}
|
|
651
677
|
return Promise.all(
|
|
652
|
-
Array.from(this.
|
|
653
|
-
|
|
678
|
+
Array.from(this.environments.values()).map((environment) =>
|
|
679
|
+
environment.serialize(),
|
|
654
680
|
),
|
|
655
681
|
);
|
|
656
682
|
}
|
|
@@ -658,23 +684,23 @@ export class ProjectStore {
|
|
|
658
684
|
public async getStatus() {
|
|
659
685
|
const status = {
|
|
660
686
|
timestamp: Date.now(),
|
|
661
|
-
|
|
687
|
+
environments: [] as Array<components["schemas"]["Environment"]>,
|
|
662
688
|
initialized: this.isInitialized,
|
|
663
689
|
frozenConfig: isPublisherConfigFrozen(this.serverRootPath),
|
|
664
690
|
operationalState:
|
|
665
691
|
getOperationalState() as components["schemas"]["ServerStatus"]["operationalState"],
|
|
666
692
|
};
|
|
667
693
|
|
|
668
|
-
const
|
|
694
|
+
const environments = await this.listEnvironments(true);
|
|
669
695
|
|
|
670
696
|
await Promise.all(
|
|
671
|
-
|
|
697
|
+
environments.map(async (environment) => {
|
|
672
698
|
try {
|
|
673
|
-
const packages =
|
|
674
|
-
const connections =
|
|
699
|
+
const packages = environment.packages;
|
|
700
|
+
const connections = environment.connections;
|
|
675
701
|
|
|
676
|
-
logger.debug(`
|
|
677
|
-
connectionsCount:
|
|
702
|
+
logger.debug(`Environment ${environment.name} status:`, {
|
|
703
|
+
connectionsCount: environment.connections?.length || 0,
|
|
678
704
|
packagesCount: packages?.length || 0,
|
|
679
705
|
});
|
|
680
706
|
|
|
@@ -685,12 +711,12 @@ export class ProjectStore {
|
|
|
685
711
|
};
|
|
686
712
|
});
|
|
687
713
|
|
|
688
|
-
const
|
|
689
|
-
...
|
|
714
|
+
const _environment = {
|
|
715
|
+
...environment,
|
|
690
716
|
connections: _connections,
|
|
691
717
|
};
|
|
692
|
-
|
|
693
|
-
status.
|
|
718
|
+
environment.connections = _connections;
|
|
719
|
+
status.environments.push(_environment);
|
|
694
720
|
} catch (error) {
|
|
695
721
|
logger.error("Error listing packages and connections", {
|
|
696
722
|
error,
|
|
@@ -704,62 +730,63 @@ export class ProjectStore {
|
|
|
704
730
|
return status;
|
|
705
731
|
}
|
|
706
732
|
|
|
707
|
-
public async
|
|
708
|
-
|
|
733
|
+
public async getEnvironment(
|
|
734
|
+
environmentName: string,
|
|
709
735
|
reload: boolean = false,
|
|
710
|
-
): Promise<
|
|
736
|
+
): Promise<Environment> {
|
|
711
737
|
await this.finishedInitialization;
|
|
712
738
|
|
|
713
|
-
// Check if
|
|
714
|
-
const
|
|
715
|
-
if (
|
|
716
|
-
return
|
|
739
|
+
// Check if environment is already loaded first
|
|
740
|
+
const environment = this.environments.get(environmentName);
|
|
741
|
+
if (environment !== undefined && !reload) {
|
|
742
|
+
return environment;
|
|
717
743
|
}
|
|
718
744
|
|
|
719
745
|
// We need to acquire the mutex to prevent concurrent requests from creating the
|
|
720
|
-
//
|
|
721
|
-
let
|
|
722
|
-
if (
|
|
723
|
-
await
|
|
724
|
-
const
|
|
725
|
-
if (
|
|
726
|
-
return
|
|
746
|
+
// environment multiple times.
|
|
747
|
+
let environmentMutex = this.environmentMutexes.get(environmentName);
|
|
748
|
+
if (environmentMutex?.isLocked()) {
|
|
749
|
+
await environmentMutex.waitForUnlock();
|
|
750
|
+
const existingEnvironment = this.environments.get(environmentName);
|
|
751
|
+
if (existingEnvironment && !reload) {
|
|
752
|
+
return existingEnvironment;
|
|
727
753
|
}
|
|
728
754
|
}
|
|
729
|
-
|
|
730
|
-
this.
|
|
755
|
+
environmentMutex = new Mutex();
|
|
756
|
+
this.environmentMutexes.set(environmentName, environmentMutex);
|
|
731
757
|
|
|
732
|
-
return
|
|
758
|
+
return environmentMutex.runExclusive(async () => {
|
|
733
759
|
// Double-check after acquiring mutex
|
|
734
|
-
const
|
|
735
|
-
if (
|
|
736
|
-
return
|
|
760
|
+
const existingEnvironment = this.environments.get(environmentName);
|
|
761
|
+
if (existingEnvironment !== undefined && !reload) {
|
|
762
|
+
return existingEnvironment;
|
|
737
763
|
}
|
|
738
764
|
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
765
|
+
const environmentManifest =
|
|
766
|
+
await EnvironmentStore.reloadEnvironmentManifest(
|
|
767
|
+
this.serverRootPath,
|
|
768
|
+
);
|
|
769
|
+
const environmentConfig = environmentManifest.environments.find(
|
|
770
|
+
(e) => e.name === environmentName,
|
|
744
771
|
);
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if (!
|
|
749
|
-
throw new
|
|
750
|
-
`
|
|
772
|
+
const environmentPath =
|
|
773
|
+
existingEnvironment?.metadata.location ||
|
|
774
|
+
environmentConfig?.packages[0]?.location;
|
|
775
|
+
if (!environmentPath) {
|
|
776
|
+
throw new EnvironmentNotFoundError(
|
|
777
|
+
`Environment "${environmentName}" could not be resolved to a path.`,
|
|
751
778
|
);
|
|
752
779
|
}
|
|
753
|
-
return await this.
|
|
754
|
-
name:
|
|
755
|
-
resource: `${API_PREFIX}/
|
|
756
|
-
connections:
|
|
780
|
+
return await this.addEnvironment({
|
|
781
|
+
name: environmentName,
|
|
782
|
+
resource: `${API_PREFIX}/environments/${environmentName}`,
|
|
783
|
+
connections: environmentConfig?.connections || [],
|
|
757
784
|
});
|
|
758
785
|
});
|
|
759
786
|
}
|
|
760
787
|
|
|
761
|
-
public async
|
|
762
|
-
|
|
788
|
+
public async addEnvironment(
|
|
789
|
+
environment: ApiEnvironment,
|
|
763
790
|
skipInitialization: boolean = false,
|
|
764
791
|
) {
|
|
765
792
|
if (!skipInitialization) {
|
|
@@ -768,136 +795,151 @@ export class ProjectStore {
|
|
|
768
795
|
if (!skipInitialization && this.publisherConfigIsFrozen) {
|
|
769
796
|
throw new FrozenConfigError();
|
|
770
797
|
}
|
|
771
|
-
const
|
|
772
|
-
if (!
|
|
773
|
-
throw new Error("
|
|
774
|
-
}
|
|
775
|
-
// Check if
|
|
776
|
-
const
|
|
777
|
-
if (
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
(
|
|
798
|
+
const environmentName = environment.name;
|
|
799
|
+
if (!environmentName) {
|
|
800
|
+
throw new Error("Environment name is required");
|
|
801
|
+
}
|
|
802
|
+
// Check if environment already exists and update it instead of creating a new one
|
|
803
|
+
const existingEnvironment = this.environments.get(environmentName);
|
|
804
|
+
if (existingEnvironment) {
|
|
805
|
+
const updatedEnvironment =
|
|
806
|
+
await existingEnvironment.update(environment);
|
|
807
|
+
this.environments.set(environmentName, updatedEnvironment);
|
|
808
|
+
await this.addEnvironmentToDatabase(updatedEnvironment);
|
|
809
|
+
return updatedEnvironment;
|
|
810
|
+
}
|
|
811
|
+
const environmentManifest =
|
|
812
|
+
await EnvironmentStore.reloadEnvironmentManifest(this.serverRootPath);
|
|
813
|
+
const environmentConfig = environmentManifest.environments.find(
|
|
814
|
+
(e) => e.name === environmentName,
|
|
788
815
|
);
|
|
789
816
|
const hasPackages =
|
|
790
|
-
(
|
|
791
|
-
(
|
|
792
|
-
let
|
|
817
|
+
(environment?.packages && environment.packages.length > 0) ||
|
|
818
|
+
(environmentConfig?.packages && environmentConfig.packages.length > 0);
|
|
819
|
+
let absoluteEnvironmentPath: string;
|
|
793
820
|
if (hasPackages) {
|
|
794
821
|
const packagesToProcess =
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
822
|
+
environment?.packages || environmentConfig?.packages || [];
|
|
823
|
+
absoluteEnvironmentPath = await this.loadEnvironmentIntoDisk(
|
|
824
|
+
environmentName,
|
|
798
825
|
packagesToProcess,
|
|
799
826
|
);
|
|
800
|
-
if (
|
|
801
|
-
|
|
827
|
+
if (absoluteEnvironmentPath.endsWith(".zip")) {
|
|
828
|
+
absoluteEnvironmentPath = await this.unzipEnvironment(
|
|
829
|
+
absoluteEnvironmentPath,
|
|
830
|
+
);
|
|
802
831
|
}
|
|
803
832
|
} else {
|
|
804
|
-
|
|
833
|
+
absoluteEnvironmentPath = await this.scaffoldEnvironment(environment);
|
|
805
834
|
}
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
835
|
+
const newEnvironment = await Environment.create(
|
|
836
|
+
environmentName,
|
|
837
|
+
absoluteEnvironmentPath,
|
|
838
|
+
environment.connections || [],
|
|
810
839
|
);
|
|
811
840
|
|
|
812
|
-
if (!
|
|
813
|
-
|
|
814
|
-
if (
|
|
815
|
-
|
|
816
|
-
|
|
841
|
+
if (!newEnvironment.metadata) newEnvironment.metadata = {};
|
|
842
|
+
newEnvironment.metadata.location = absoluteEnvironmentPath;
|
|
843
|
+
if (environment.materializationStorage !== undefined) {
|
|
844
|
+
newEnvironment.metadata.materializationStorage =
|
|
845
|
+
environment.materializationStorage;
|
|
817
846
|
}
|
|
818
847
|
|
|
819
|
-
this.
|
|
848
|
+
this.environments.set(environmentName, newEnvironment);
|
|
820
849
|
|
|
821
|
-
|
|
850
|
+
environment?.packages?.forEach((_package) => {
|
|
822
851
|
if (_package.name) {
|
|
823
|
-
|
|
852
|
+
newEnvironment.setPackageStatus(
|
|
853
|
+
_package.name,
|
|
854
|
+
PackageStatus.SERVING,
|
|
855
|
+
);
|
|
824
856
|
}
|
|
825
857
|
});
|
|
826
858
|
|
|
827
|
-
await this.
|
|
859
|
+
await this.addEnvironmentToDatabase(newEnvironment);
|
|
828
860
|
|
|
829
|
-
return
|
|
861
|
+
return newEnvironment;
|
|
830
862
|
}
|
|
831
863
|
|
|
832
|
-
public async
|
|
864
|
+
public async unzipEnvironment(absoluteEnvironmentPath: string) {
|
|
833
865
|
logger.info(
|
|
834
|
-
`Detected zip file at "${
|
|
866
|
+
`Detected zip file at "${absoluteEnvironmentPath}". Unzipping...`,
|
|
867
|
+
);
|
|
868
|
+
const unzippedEnvironmentPath = absoluteEnvironmentPath.replace(
|
|
869
|
+
".zip",
|
|
870
|
+
"",
|
|
835
871
|
);
|
|
836
|
-
|
|
837
|
-
await fs.promises.rm(unzippedProjectPath, {
|
|
872
|
+
await fs.promises.rm(unzippedEnvironmentPath, {
|
|
838
873
|
recursive: true,
|
|
839
874
|
force: true,
|
|
840
875
|
});
|
|
841
|
-
await fs.promises.mkdir(
|
|
876
|
+
await fs.promises.mkdir(unzippedEnvironmentPath, { recursive: true });
|
|
842
877
|
|
|
843
|
-
const zip = new AdmZip(
|
|
844
|
-
zip.extractAllTo(
|
|
878
|
+
const zip = new AdmZip(absoluteEnvironmentPath);
|
|
879
|
+
zip.extractAllTo(unzippedEnvironmentPath, true);
|
|
845
880
|
|
|
846
|
-
return
|
|
881
|
+
return unzippedEnvironmentPath;
|
|
847
882
|
}
|
|
848
883
|
|
|
849
|
-
public async
|
|
884
|
+
public async updateEnvironment(environment: ApiEnvironment) {
|
|
850
885
|
await this.finishedInitialization;
|
|
851
886
|
if (this.publisherConfigIsFrozen) {
|
|
852
887
|
throw new FrozenConfigError();
|
|
853
888
|
}
|
|
854
|
-
|
|
855
|
-
const
|
|
856
|
-
if (!
|
|
857
|
-
throw new Error("
|
|
889
|
+
validateEnvironmentAzureUrls(environment);
|
|
890
|
+
const environmentName = environment.name;
|
|
891
|
+
if (!environmentName) {
|
|
892
|
+
throw new Error("Environment name is required");
|
|
858
893
|
}
|
|
859
|
-
const
|
|
860
|
-
if (!
|
|
861
|
-
throw new
|
|
894
|
+
const existingEnvironment = this.environments.get(environmentName);
|
|
895
|
+
if (!existingEnvironment) {
|
|
896
|
+
throw new EnvironmentNotFoundError(
|
|
897
|
+
`Environment ${environmentName} not found`,
|
|
898
|
+
);
|
|
862
899
|
}
|
|
863
|
-
const
|
|
864
|
-
this.
|
|
865
|
-
await this.
|
|
866
|
-
return
|
|
900
|
+
const updatedEnvironment = await existingEnvironment.update(environment);
|
|
901
|
+
this.environments.set(environmentName, updatedEnvironment);
|
|
902
|
+
await this.addEnvironmentToDatabase(updatedEnvironment);
|
|
903
|
+
return updatedEnvironment;
|
|
867
904
|
}
|
|
868
905
|
|
|
869
|
-
public async
|
|
870
|
-
|
|
871
|
-
): Promise<
|
|
906
|
+
public async deleteEnvironment(
|
|
907
|
+
environmentName: string,
|
|
908
|
+
): Promise<Environment | undefined> {
|
|
872
909
|
await this.finishedInitialization;
|
|
873
910
|
if (this.publisherConfigIsFrozen) {
|
|
874
911
|
throw new FrozenConfigError();
|
|
875
912
|
}
|
|
876
|
-
const
|
|
877
|
-
if (!
|
|
913
|
+
const environment = this.environments.get(environmentName);
|
|
914
|
+
if (!environment) {
|
|
878
915
|
return;
|
|
879
916
|
}
|
|
880
917
|
|
|
881
|
-
const
|
|
918
|
+
const environmentPath = environment.metadata?.location;
|
|
882
919
|
|
|
883
|
-
// Close all connections before removing the
|
|
884
|
-
await
|
|
920
|
+
// Close all connections before removing the environment
|
|
921
|
+
await environment.closeAllConnections();
|
|
885
922
|
|
|
886
|
-
this.
|
|
887
|
-
await this.
|
|
888
|
-
if (
|
|
923
|
+
this.environments.delete(environmentName);
|
|
924
|
+
await this.deleteEnvironmentFromDatabase(environmentName);
|
|
925
|
+
if (environmentPath) {
|
|
889
926
|
try {
|
|
890
|
-
await fs.promises.rm(
|
|
891
|
-
|
|
927
|
+
await fs.promises.rm(environmentPath, {
|
|
928
|
+
recursive: true,
|
|
929
|
+
force: true,
|
|
930
|
+
});
|
|
931
|
+
logger.info(`Deleted environment directory: ${environmentPath}`);
|
|
892
932
|
} catch (err) {
|
|
893
|
-
logger.error("Error removing
|
|
933
|
+
logger.error("Error removing environment directory", {
|
|
934
|
+
error: err,
|
|
935
|
+
});
|
|
894
936
|
}
|
|
895
937
|
}
|
|
896
938
|
|
|
897
|
-
return
|
|
939
|
+
return environment;
|
|
898
940
|
}
|
|
899
941
|
|
|
900
|
-
public static async
|
|
942
|
+
public static async reloadEnvironmentManifest(
|
|
901
943
|
serverRootPath: string,
|
|
902
944
|
): Promise<ProcessedPublisherConfig> {
|
|
903
945
|
try {
|
|
@@ -908,17 +950,17 @@ export class ProjectStore {
|
|
|
908
950
|
`Error reading ${PUBLISHER_CONFIG_NAME}. Generating from directory`,
|
|
909
951
|
{ error },
|
|
910
952
|
);
|
|
911
|
-
return { frozenConfig: false,
|
|
953
|
+
return { frozenConfig: false, environments: [] };
|
|
912
954
|
} else {
|
|
913
955
|
// If publisher.config.json is missing, generate the manifest from directories
|
|
914
956
|
try {
|
|
915
957
|
const entries = await fs.promises.readdir(serverRootPath, {
|
|
916
958
|
withFileTypes: true,
|
|
917
959
|
});
|
|
918
|
-
const
|
|
960
|
+
const environments: ProcessedEnvironment[] = [];
|
|
919
961
|
for (const entry of entries) {
|
|
920
962
|
if (entry.isDirectory()) {
|
|
921
|
-
|
|
963
|
+
environments.push({
|
|
922
964
|
name: entry.name,
|
|
923
965
|
packages: [
|
|
924
966
|
{
|
|
@@ -930,31 +972,31 @@ export class ProjectStore {
|
|
|
930
972
|
});
|
|
931
973
|
}
|
|
932
974
|
}
|
|
933
|
-
return { frozenConfig: false,
|
|
975
|
+
return { frozenConfig: false, environments };
|
|
934
976
|
} catch (lsError) {
|
|
935
977
|
logger.error(`Error listing directories in ${serverRootPath}`, {
|
|
936
978
|
error: lsError,
|
|
937
979
|
});
|
|
938
|
-
return { frozenConfig: false,
|
|
980
|
+
return { frozenConfig: false, environments: [] };
|
|
939
981
|
}
|
|
940
982
|
}
|
|
941
983
|
}
|
|
942
984
|
}
|
|
943
985
|
|
|
944
|
-
private async
|
|
945
|
-
const
|
|
946
|
-
if (!
|
|
947
|
-
throw new Error("
|
|
986
|
+
private async scaffoldEnvironment(environment: ApiEnvironment) {
|
|
987
|
+
const environmentName = environment.name;
|
|
988
|
+
if (!environmentName) {
|
|
989
|
+
throw new Error("Environment name is required");
|
|
948
990
|
}
|
|
949
|
-
const
|
|
950
|
-
await fs.promises.mkdir(
|
|
951
|
-
if (
|
|
991
|
+
const absoluteEnvironmentPath = `${this.serverRootPath}/${PUBLISHER_DATA_DIR}/${environmentName}`;
|
|
992
|
+
await fs.promises.mkdir(absoluteEnvironmentPath, { recursive: true });
|
|
993
|
+
if (environment.readme) {
|
|
952
994
|
await fs.promises.writeFile(
|
|
953
|
-
path.join(
|
|
954
|
-
|
|
995
|
+
path.join(absoluteEnvironmentPath, "README.md"),
|
|
996
|
+
environment.readme,
|
|
955
997
|
);
|
|
956
998
|
}
|
|
957
|
-
return
|
|
999
|
+
return absoluteEnvironmentPath;
|
|
958
1000
|
}
|
|
959
1001
|
|
|
960
1002
|
private isLocalPath(location: string) {
|
|
@@ -982,17 +1024,17 @@ export class ProjectStore {
|
|
|
982
1024
|
return location.startsWith("s3://");
|
|
983
1025
|
}
|
|
984
1026
|
|
|
985
|
-
private async
|
|
986
|
-
|
|
987
|
-
packages:
|
|
1027
|
+
private async loadEnvironmentIntoDisk(
|
|
1028
|
+
environmentName: string,
|
|
1029
|
+
packages: ApiEnvironment["packages"],
|
|
988
1030
|
) {
|
|
989
|
-
const absoluteTargetPath = `${this.serverRootPath}/${PUBLISHER_DATA_DIR}/${
|
|
1031
|
+
const absoluteTargetPath = `${this.serverRootPath}/${PUBLISHER_DATA_DIR}/${environmentName}`;
|
|
990
1032
|
|
|
991
1033
|
await fs.promises.mkdir(absoluteTargetPath, { recursive: true });
|
|
992
1034
|
|
|
993
1035
|
if (!packages || packages.length === 0) {
|
|
994
1036
|
throw new PackageNotFoundError(
|
|
995
|
-
`No packages found for
|
|
1037
|
+
`No packages found for environment ${environmentName}`,
|
|
996
1038
|
);
|
|
997
1039
|
}
|
|
998
1040
|
|
|
@@ -1049,7 +1091,7 @@ export class ProjectStore {
|
|
|
1049
1091
|
await this.downloadOrMountLocation(
|
|
1050
1092
|
groupedLocation,
|
|
1051
1093
|
tempDownloadPath,
|
|
1052
|
-
|
|
1094
|
+
environmentName,
|
|
1053
1095
|
"shared",
|
|
1054
1096
|
);
|
|
1055
1097
|
// Extract each package from the downloaded content
|
|
@@ -1148,7 +1190,7 @@ export class ProjectStore {
|
|
|
1148
1190
|
private async downloadOrMountLocation(
|
|
1149
1191
|
location: string,
|
|
1150
1192
|
targetPath: string,
|
|
1151
|
-
|
|
1193
|
+
environmentName: string,
|
|
1152
1194
|
packageName: string,
|
|
1153
1195
|
) {
|
|
1154
1196
|
const isCompressedFile = location.endsWith(".zip");
|
|
@@ -1160,7 +1202,7 @@ export class ProjectStore {
|
|
|
1160
1202
|
);
|
|
1161
1203
|
await this.downloadGcsDirectory(
|
|
1162
1204
|
location,
|
|
1163
|
-
|
|
1205
|
+
environmentName,
|
|
1164
1206
|
targetPath,
|
|
1165
1207
|
isCompressedFile,
|
|
1166
1208
|
);
|
|
@@ -1205,7 +1247,7 @@ export class ProjectStore {
|
|
|
1205
1247
|
);
|
|
1206
1248
|
await this.downloadS3Directory(
|
|
1207
1249
|
location,
|
|
1208
|
-
|
|
1250
|
+
environmentName,
|
|
1209
1251
|
targetPath,
|
|
1210
1252
|
isCompressedFile,
|
|
1211
1253
|
);
|
|
@@ -1234,7 +1276,7 @@ export class ProjectStore {
|
|
|
1234
1276
|
await this.mountLocalDirectory(
|
|
1235
1277
|
packagePath,
|
|
1236
1278
|
targetPath,
|
|
1237
|
-
|
|
1279
|
+
environmentName,
|
|
1238
1280
|
packageName,
|
|
1239
1281
|
);
|
|
1240
1282
|
return;
|
|
@@ -1252,40 +1294,40 @@ export class ProjectStore {
|
|
|
1252
1294
|
|
|
1253
1295
|
// If we get here, the path format is not supported
|
|
1254
1296
|
const errorMsg = `Invalid package path: "${location}". Must be an absolute mounted path or a GCS/S3/GitHub URI.`;
|
|
1255
|
-
logger.error(errorMsg, {
|
|
1297
|
+
logger.error(errorMsg, { environmentName, location });
|
|
1256
1298
|
throw new PackageNotFoundError(errorMsg);
|
|
1257
1299
|
}
|
|
1258
1300
|
|
|
1259
1301
|
public async mountLocalDirectory(
|
|
1260
|
-
|
|
1302
|
+
environmentPath: string,
|
|
1261
1303
|
absoluteTargetPath: string,
|
|
1262
|
-
|
|
1304
|
+
environmentName: string,
|
|
1263
1305
|
packageName: string,
|
|
1264
1306
|
) {
|
|
1265
|
-
if (
|
|
1266
|
-
|
|
1307
|
+
if (environmentPath.endsWith(".zip")) {
|
|
1308
|
+
environmentPath = await this.unzipEnvironment(environmentPath);
|
|
1267
1309
|
}
|
|
1268
|
-
const
|
|
1269
|
-
(await fs.promises.stat(
|
|
1270
|
-
if (
|
|
1310
|
+
const environmentDirExists =
|
|
1311
|
+
(await fs.promises.stat(environmentPath))?.isDirectory() ?? false;
|
|
1312
|
+
if (environmentDirExists) {
|
|
1271
1313
|
await fs.promises.rm(absoluteTargetPath, {
|
|
1272
1314
|
recursive: true,
|
|
1273
1315
|
force: true,
|
|
1274
1316
|
});
|
|
1275
1317
|
await fs.promises.mkdir(absoluteTargetPath, { recursive: true });
|
|
1276
|
-
await fs.promises.cp(
|
|
1318
|
+
await fs.promises.cp(environmentPath, absoluteTargetPath, {
|
|
1277
1319
|
recursive: true,
|
|
1278
1320
|
});
|
|
1279
1321
|
} else {
|
|
1280
1322
|
throw new PackageNotFoundError(
|
|
1281
|
-
`Package ${packageName} for
|
|
1323
|
+
`Package ${packageName} for environment ${environmentName} not found in "${environmentPath}"`,
|
|
1282
1324
|
);
|
|
1283
1325
|
}
|
|
1284
1326
|
}
|
|
1285
1327
|
|
|
1286
1328
|
async downloadGcsDirectory(
|
|
1287
1329
|
gcsPath: string,
|
|
1288
|
-
|
|
1330
|
+
environmentName: string,
|
|
1289
1331
|
absoluteDirPath: string,
|
|
1290
1332
|
isCompressedFile: boolean,
|
|
1291
1333
|
) {
|
|
@@ -1296,8 +1338,8 @@ export class ProjectStore {
|
|
|
1296
1338
|
prefix,
|
|
1297
1339
|
});
|
|
1298
1340
|
if (files.length === 0) {
|
|
1299
|
-
throw new
|
|
1300
|
-
`
|
|
1341
|
+
throw new EnvironmentNotFoundError(
|
|
1342
|
+
`Environment ${environmentName} not found in ${gcsPath}`,
|
|
1301
1343
|
);
|
|
1302
1344
|
}
|
|
1303
1345
|
if (!isCompressedFile) {
|
|
@@ -1328,14 +1370,14 @@ export class ProjectStore {
|
|
|
1328
1370
|
}),
|
|
1329
1371
|
);
|
|
1330
1372
|
if (isCompressedFile) {
|
|
1331
|
-
await this.
|
|
1373
|
+
await this.unzipEnvironment(absoluteDirPath);
|
|
1332
1374
|
}
|
|
1333
1375
|
logger.info(`Downloaded GCS directory ${gcsPath} to ${absoluteDirPath}`);
|
|
1334
1376
|
}
|
|
1335
1377
|
|
|
1336
1378
|
async downloadS3Directory(
|
|
1337
1379
|
s3Path: string,
|
|
1338
|
-
|
|
1380
|
+
environmentName: string,
|
|
1339
1381
|
absoluteDirPath: string,
|
|
1340
1382
|
isCompressedFile: boolean = false,
|
|
1341
1383
|
) {
|
|
@@ -1356,8 +1398,8 @@ export class ProjectStore {
|
|
|
1356
1398
|
});
|
|
1357
1399
|
const item = await this.s3Client.send(command);
|
|
1358
1400
|
if (!item.Body) {
|
|
1359
|
-
throw new
|
|
1360
|
-
`
|
|
1401
|
+
throw new EnvironmentNotFoundError(
|
|
1402
|
+
`Environment ${environmentName} not found in ${s3Path}`,
|
|
1361
1403
|
);
|
|
1362
1404
|
}
|
|
1363
1405
|
const file = fs.createWriteStream(zipFilePath);
|
|
@@ -1368,7 +1410,7 @@ export class ProjectStore {
|
|
|
1368
1410
|
});
|
|
1369
1411
|
|
|
1370
1412
|
// Extract the zip file
|
|
1371
|
-
await this.
|
|
1413
|
+
await this.unzipEnvironment(zipFilePath);
|
|
1372
1414
|
logger.info(`Downloaded S3 zip file ${s3Path} to ${absoluteDirPath}`);
|
|
1373
1415
|
return;
|
|
1374
1416
|
}
|
|
@@ -1382,8 +1424,8 @@ export class ProjectStore {
|
|
|
1382
1424
|
await fs.promises.mkdir(absoluteDirPath, { recursive: true });
|
|
1383
1425
|
|
|
1384
1426
|
if (!objects.Contents || objects.Contents.length === 0) {
|
|
1385
|
-
throw new
|
|
1386
|
-
`
|
|
1427
|
+
throw new EnvironmentNotFoundError(
|
|
1428
|
+
`Environment ${environmentName} not found in ${s3Path}`,
|
|
1387
1429
|
);
|
|
1388
1430
|
}
|
|
1389
1431
|
await Promise.all(
|