@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
|
@@ -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
|
+
);
|
|
193
195
|
|
|
194
|
-
if (
|
|
195
|
-
const
|
|
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
|
+
);
|
|
208
|
+
|
|
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,21 +378,25 @@ export class ProjectStore {
|
|
|
359
378
|
materializationStorage?.catalogUrl &&
|
|
360
379
|
materializationStorage?.dataPath
|
|
361
380
|
) {
|
|
362
|
-
await this.storageManager.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
381
|
+
await this.storageManager.initializeDuckLakeForEnvironment(
|
|
382
|
+
dbEnvironment.id,
|
|
383
|
+
dbEnvironment.name,
|
|
384
|
+
{
|
|
385
|
+
catalogUrl: materializationStorage.catalogUrl,
|
|
386
|
+
dataPath: materializationStorage.dataPath,
|
|
387
|
+
},
|
|
388
|
+
);
|
|
366
389
|
}
|
|
367
390
|
|
|
368
|
-
return
|
|
391
|
+
return dbEnvironment;
|
|
369
392
|
}
|
|
370
393
|
|
|
371
394
|
private async addPackages(
|
|
372
|
-
|
|
373
|
-
|
|
395
|
+
environment: Environment,
|
|
396
|
+
environmentId: string,
|
|
374
397
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
375
398
|
): Promise<void> {
|
|
376
|
-
const packages = await
|
|
399
|
+
const packages = await environment.listPackages();
|
|
377
400
|
|
|
378
401
|
// Sync each package
|
|
379
402
|
for (const pkg of packages) {
|
|
@@ -382,13 +405,13 @@ export class ProjectStore {
|
|
|
382
405
|
continue;
|
|
383
406
|
}
|
|
384
407
|
|
|
385
|
-
await this.addPackage(pkg,
|
|
408
|
+
await this.addPackage(pkg, environmentId, repository);
|
|
386
409
|
}
|
|
387
410
|
}
|
|
388
411
|
|
|
389
412
|
private async addPackage(
|
|
390
413
|
pkg: components["schemas"]["Package"],
|
|
391
|
-
|
|
414
|
+
environmentId: string,
|
|
392
415
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
393
416
|
): Promise<void> {
|
|
394
417
|
const pkgs = pkg as {
|
|
@@ -399,7 +422,7 @@ export class ProjectStore {
|
|
|
399
422
|
};
|
|
400
423
|
|
|
401
424
|
const packageData = {
|
|
402
|
-
|
|
425
|
+
environmentId,
|
|
403
426
|
name: pkgs.name,
|
|
404
427
|
description: pkgs.description ?? undefined,
|
|
405
428
|
manifestPath: pkgs.manifestPath ?? "",
|
|
@@ -417,7 +440,7 @@ export class ProjectStore {
|
|
|
417
440
|
) {
|
|
418
441
|
await this.updatePackage(
|
|
419
442
|
pkgs.name,
|
|
420
|
-
|
|
443
|
+
environmentId,
|
|
421
444
|
packageData,
|
|
422
445
|
repository,
|
|
423
446
|
);
|
|
@@ -429,7 +452,7 @@ export class ProjectStore {
|
|
|
429
452
|
|
|
430
453
|
private async updatePackage(
|
|
431
454
|
packageName: string,
|
|
432
|
-
|
|
455
|
+
environmentId: string,
|
|
433
456
|
packageData: {
|
|
434
457
|
description: string | undefined;
|
|
435
458
|
manifestPath: string;
|
|
@@ -438,7 +461,7 @@ export class ProjectStore {
|
|
|
438
461
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
439
462
|
): Promise<void> {
|
|
440
463
|
const existingPackage = await repository.getPackageByName(
|
|
441
|
-
|
|
464
|
+
environmentId,
|
|
442
465
|
packageName,
|
|
443
466
|
);
|
|
444
467
|
|
|
@@ -449,12 +472,12 @@ export class ProjectStore {
|
|
|
449
472
|
}
|
|
450
473
|
|
|
451
474
|
private async addConnections(
|
|
452
|
-
|
|
453
|
-
|
|
475
|
+
environment: Environment,
|
|
476
|
+
environmentId: string,
|
|
454
477
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
455
478
|
): Promise<void> {
|
|
456
479
|
try {
|
|
457
|
-
const connections =
|
|
480
|
+
const connections = environment.listApiConnections();
|
|
458
481
|
// Add/update connections
|
|
459
482
|
for (const conn of connections) {
|
|
460
483
|
if (!conn.name) {
|
|
@@ -464,26 +487,29 @@ export class ProjectStore {
|
|
|
464
487
|
|
|
465
488
|
// Check if connection exists
|
|
466
489
|
const existingConn = await repository.getConnectionByName(
|
|
467
|
-
|
|
490
|
+
environmentId,
|
|
468
491
|
conn.name,
|
|
469
492
|
);
|
|
470
493
|
|
|
471
494
|
if (existingConn) {
|
|
472
|
-
await this.updateConnection(conn,
|
|
495
|
+
await this.updateConnection(conn, environmentId, repository);
|
|
473
496
|
} else {
|
|
474
|
-
await this.addConnection(conn,
|
|
497
|
+
await this.addConnection(conn, environmentId, repository);
|
|
475
498
|
}
|
|
476
499
|
}
|
|
477
500
|
} catch (err: unknown) {
|
|
478
501
|
const error = err as Error;
|
|
479
|
-
const
|
|
480
|
-
logger.error(
|
|
502
|
+
const environmentName = environment.metadata?.name;
|
|
503
|
+
logger.error(
|
|
504
|
+
`Error syncing connections for "${environmentName}":`,
|
|
505
|
+
error,
|
|
506
|
+
);
|
|
481
507
|
}
|
|
482
508
|
}
|
|
483
509
|
|
|
484
510
|
public async addConnection(
|
|
485
|
-
conn: ReturnType<
|
|
486
|
-
|
|
511
|
+
conn: ReturnType<Environment["listApiConnections"]>[number],
|
|
512
|
+
environmentId: string,
|
|
487
513
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
488
514
|
): Promise<void> {
|
|
489
515
|
if (!conn.name) {
|
|
@@ -492,7 +518,7 @@ export class ProjectStore {
|
|
|
492
518
|
}
|
|
493
519
|
|
|
494
520
|
const connectionData = {
|
|
495
|
-
|
|
521
|
+
environmentId,
|
|
496
522
|
name: conn.name,
|
|
497
523
|
type: conn.type as Connection["type"],
|
|
498
524
|
config: conn,
|
|
@@ -509,8 +535,8 @@ export class ProjectStore {
|
|
|
509
535
|
}
|
|
510
536
|
|
|
511
537
|
public async updateConnection(
|
|
512
|
-
conn: ReturnType<
|
|
513
|
-
|
|
538
|
+
conn: ReturnType<Environment["listApiConnections"]>[number],
|
|
539
|
+
environmentId: string,
|
|
514
540
|
repository: ReturnType<typeof this.storageManager.getRepository>,
|
|
515
541
|
): Promise<void> {
|
|
516
542
|
if (!conn.name) {
|
|
@@ -518,12 +544,12 @@ export class ProjectStore {
|
|
|
518
544
|
}
|
|
519
545
|
|
|
520
546
|
const existingConn = await repository.getConnectionByName(
|
|
521
|
-
|
|
547
|
+
environmentId,
|
|
522
548
|
conn.name,
|
|
523
549
|
);
|
|
524
550
|
|
|
525
551
|
if (!existingConn) {
|
|
526
|
-
logger.error(`Connection "${conn.name}" not found in
|
|
552
|
+
logger.error(`Connection "${conn.name}" not found in environment`);
|
|
527
553
|
}
|
|
528
554
|
|
|
529
555
|
const connectionData = {
|
|
@@ -544,31 +570,34 @@ export class ProjectStore {
|
|
|
544
570
|
}
|
|
545
571
|
|
|
546
572
|
public async addPackageToDatabase(
|
|
547
|
-
|
|
573
|
+
environmentName: string,
|
|
548
574
|
packageName: string,
|
|
549
575
|
): Promise<void> {
|
|
550
|
-
const
|
|
576
|
+
const environment = await this.getEnvironment(environmentName, false);
|
|
551
577
|
const repository = this.storageManager.getRepository();
|
|
552
578
|
|
|
553
|
-
// Get the
|
|
554
|
-
const
|
|
579
|
+
// Get the environment ID from database
|
|
580
|
+
const dbEnvironment =
|
|
581
|
+
await repository.getEnvironmentByName(environmentName);
|
|
555
582
|
|
|
556
|
-
if (!
|
|
557
|
-
logger.error(`
|
|
558
|
-
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
|
+
);
|
|
559
588
|
}
|
|
560
589
|
|
|
561
|
-
// Get the package from the
|
|
562
|
-
const packages = await
|
|
590
|
+
// Get the package from the environment
|
|
591
|
+
const packages = await environment.listPackages();
|
|
563
592
|
const pkg = packages.find((p) => p.name === packageName);
|
|
564
593
|
|
|
565
594
|
if (!pkg) {
|
|
566
|
-
logger.warn(`Package "${packageName}" not found in
|
|
595
|
+
logger.warn(`Package "${packageName}" not found in environment`);
|
|
567
596
|
return;
|
|
568
597
|
}
|
|
569
598
|
|
|
570
599
|
// Sync the specific package
|
|
571
|
-
await this.addPackage(pkg,
|
|
600
|
+
await this.addPackage(pkg, dbEnvironment.id, repository);
|
|
572
601
|
logger.info(`Synced package "${packageName}" to database`);
|
|
573
602
|
}
|
|
574
603
|
|
|
@@ -576,22 +605,23 @@ export class ProjectStore {
|
|
|
576
605
|
* Delete a package from the database
|
|
577
606
|
*/
|
|
578
607
|
public async deletePackageFromDatabase(
|
|
579
|
-
|
|
608
|
+
environmentName: string,
|
|
580
609
|
packageName: string,
|
|
581
610
|
): Promise<void> {
|
|
582
611
|
const repository = this.storageManager.getRepository();
|
|
583
612
|
|
|
584
|
-
// Get the
|
|
585
|
-
const
|
|
613
|
+
// Get the environment ID from database
|
|
614
|
+
const dbEnvironment =
|
|
615
|
+
await repository.getEnvironmentByName(environmentName);
|
|
586
616
|
|
|
587
|
-
if (!
|
|
588
|
-
logger.error(`
|
|
617
|
+
if (!dbEnvironment) {
|
|
618
|
+
logger.error(`Environment "${environmentName}" not found in database`);
|
|
589
619
|
return;
|
|
590
620
|
}
|
|
591
621
|
|
|
592
622
|
// Find and delete the package
|
|
593
623
|
const existingPackage = await repository.getPackageByName(
|
|
594
|
-
|
|
624
|
+
dbEnvironment.id,
|
|
595
625
|
packageName,
|
|
596
626
|
);
|
|
597
627
|
|
|
@@ -640,13 +670,13 @@ export class ProjectStore {
|
|
|
640
670
|
await fs.promises.mkdir(uploadDocsPath, { recursive: true });
|
|
641
671
|
}
|
|
642
672
|
|
|
643
|
-
public async
|
|
673
|
+
public async listEnvironments(skipInitializationCheck: boolean = false) {
|
|
644
674
|
if (!skipInitializationCheck) {
|
|
645
675
|
await this.finishedInitialization;
|
|
646
676
|
}
|
|
647
677
|
return Promise.all(
|
|
648
|
-
Array.from(this.
|
|
649
|
-
|
|
678
|
+
Array.from(this.environments.values()).map((environment) =>
|
|
679
|
+
environment.serialize(),
|
|
650
680
|
),
|
|
651
681
|
);
|
|
652
682
|
}
|
|
@@ -654,23 +684,23 @@ export class ProjectStore {
|
|
|
654
684
|
public async getStatus() {
|
|
655
685
|
const status = {
|
|
656
686
|
timestamp: Date.now(),
|
|
657
|
-
|
|
687
|
+
environments: [] as Array<components["schemas"]["Environment"]>,
|
|
658
688
|
initialized: this.isInitialized,
|
|
659
689
|
frozenConfig: isPublisherConfigFrozen(this.serverRootPath),
|
|
660
690
|
operationalState:
|
|
661
691
|
getOperationalState() as components["schemas"]["ServerStatus"]["operationalState"],
|
|
662
692
|
};
|
|
663
693
|
|
|
664
|
-
const
|
|
694
|
+
const environments = await this.listEnvironments(true);
|
|
665
695
|
|
|
666
696
|
await Promise.all(
|
|
667
|
-
|
|
697
|
+
environments.map(async (environment) => {
|
|
668
698
|
try {
|
|
669
|
-
const packages =
|
|
670
|
-
const connections =
|
|
699
|
+
const packages = environment.packages;
|
|
700
|
+
const connections = environment.connections;
|
|
671
701
|
|
|
672
|
-
logger.debug(`
|
|
673
|
-
connectionsCount:
|
|
702
|
+
logger.debug(`Environment ${environment.name} status:`, {
|
|
703
|
+
connectionsCount: environment.connections?.length || 0,
|
|
674
704
|
packagesCount: packages?.length || 0,
|
|
675
705
|
});
|
|
676
706
|
|
|
@@ -681,12 +711,12 @@ export class ProjectStore {
|
|
|
681
711
|
};
|
|
682
712
|
});
|
|
683
713
|
|
|
684
|
-
const
|
|
685
|
-
...
|
|
714
|
+
const _environment = {
|
|
715
|
+
...environment,
|
|
686
716
|
connections: _connections,
|
|
687
717
|
};
|
|
688
|
-
|
|
689
|
-
status.
|
|
718
|
+
environment.connections = _connections;
|
|
719
|
+
status.environments.push(_environment);
|
|
690
720
|
} catch (error) {
|
|
691
721
|
logger.error("Error listing packages and connections", {
|
|
692
722
|
error,
|
|
@@ -700,62 +730,63 @@ export class ProjectStore {
|
|
|
700
730
|
return status;
|
|
701
731
|
}
|
|
702
732
|
|
|
703
|
-
public async
|
|
704
|
-
|
|
733
|
+
public async getEnvironment(
|
|
734
|
+
environmentName: string,
|
|
705
735
|
reload: boolean = false,
|
|
706
|
-
): Promise<
|
|
736
|
+
): Promise<Environment> {
|
|
707
737
|
await this.finishedInitialization;
|
|
708
738
|
|
|
709
|
-
// Check if
|
|
710
|
-
const
|
|
711
|
-
if (
|
|
712
|
-
return
|
|
739
|
+
// Check if environment is already loaded first
|
|
740
|
+
const environment = this.environments.get(environmentName);
|
|
741
|
+
if (environment !== undefined && !reload) {
|
|
742
|
+
return environment;
|
|
713
743
|
}
|
|
714
744
|
|
|
715
745
|
// We need to acquire the mutex to prevent concurrent requests from creating the
|
|
716
|
-
//
|
|
717
|
-
let
|
|
718
|
-
if (
|
|
719
|
-
await
|
|
720
|
-
const
|
|
721
|
-
if (
|
|
722
|
-
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;
|
|
723
753
|
}
|
|
724
754
|
}
|
|
725
|
-
|
|
726
|
-
this.
|
|
755
|
+
environmentMutex = new Mutex();
|
|
756
|
+
this.environmentMutexes.set(environmentName, environmentMutex);
|
|
727
757
|
|
|
728
|
-
return
|
|
758
|
+
return environmentMutex.runExclusive(async () => {
|
|
729
759
|
// Double-check after acquiring mutex
|
|
730
|
-
const
|
|
731
|
-
if (
|
|
732
|
-
return
|
|
760
|
+
const existingEnvironment = this.environments.get(environmentName);
|
|
761
|
+
if (existingEnvironment !== undefined && !reload) {
|
|
762
|
+
return existingEnvironment;
|
|
733
763
|
}
|
|
734
764
|
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
765
|
+
const environmentManifest =
|
|
766
|
+
await EnvironmentStore.reloadEnvironmentManifest(
|
|
767
|
+
this.serverRootPath,
|
|
768
|
+
);
|
|
769
|
+
const environmentConfig = environmentManifest.environments.find(
|
|
770
|
+
(e) => e.name === environmentName,
|
|
740
771
|
);
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (!
|
|
745
|
-
throw new
|
|
746
|
-
`
|
|
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.`,
|
|
747
778
|
);
|
|
748
779
|
}
|
|
749
|
-
return await this.
|
|
750
|
-
name:
|
|
751
|
-
resource: `${API_PREFIX}/
|
|
752
|
-
connections:
|
|
780
|
+
return await this.addEnvironment({
|
|
781
|
+
name: environmentName,
|
|
782
|
+
resource: `${API_PREFIX}/environments/${environmentName}`,
|
|
783
|
+
connections: environmentConfig?.connections || [],
|
|
753
784
|
});
|
|
754
785
|
});
|
|
755
786
|
}
|
|
756
787
|
|
|
757
|
-
public async
|
|
758
|
-
|
|
788
|
+
public async addEnvironment(
|
|
789
|
+
environment: ApiEnvironment,
|
|
759
790
|
skipInitialization: boolean = false,
|
|
760
791
|
) {
|
|
761
792
|
if (!skipInitialization) {
|
|
@@ -764,136 +795,151 @@ export class ProjectStore {
|
|
|
764
795
|
if (!skipInitialization && this.publisherConfigIsFrozen) {
|
|
765
796
|
throw new FrozenConfigError();
|
|
766
797
|
}
|
|
767
|
-
const
|
|
768
|
-
if (!
|
|
769
|
-
throw new Error("
|
|
770
|
-
}
|
|
771
|
-
// Check if
|
|
772
|
-
const
|
|
773
|
-
if (
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
(
|
|
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,
|
|
784
815
|
);
|
|
785
816
|
const hasPackages =
|
|
786
|
-
(
|
|
787
|
-
(
|
|
788
|
-
let
|
|
817
|
+
(environment?.packages && environment.packages.length > 0) ||
|
|
818
|
+
(environmentConfig?.packages && environmentConfig.packages.length > 0);
|
|
819
|
+
let absoluteEnvironmentPath: string;
|
|
789
820
|
if (hasPackages) {
|
|
790
821
|
const packagesToProcess =
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
822
|
+
environment?.packages || environmentConfig?.packages || [];
|
|
823
|
+
absoluteEnvironmentPath = await this.loadEnvironmentIntoDisk(
|
|
824
|
+
environmentName,
|
|
794
825
|
packagesToProcess,
|
|
795
826
|
);
|
|
796
|
-
if (
|
|
797
|
-
|
|
827
|
+
if (absoluteEnvironmentPath.endsWith(".zip")) {
|
|
828
|
+
absoluteEnvironmentPath = await this.unzipEnvironment(
|
|
829
|
+
absoluteEnvironmentPath,
|
|
830
|
+
);
|
|
798
831
|
}
|
|
799
832
|
} else {
|
|
800
|
-
|
|
833
|
+
absoluteEnvironmentPath = await this.scaffoldEnvironment(environment);
|
|
801
834
|
}
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
835
|
+
const newEnvironment = await Environment.create(
|
|
836
|
+
environmentName,
|
|
837
|
+
absoluteEnvironmentPath,
|
|
838
|
+
environment.connections || [],
|
|
806
839
|
);
|
|
807
840
|
|
|
808
|
-
if (!
|
|
809
|
-
|
|
810
|
-
if (
|
|
811
|
-
|
|
812
|
-
|
|
841
|
+
if (!newEnvironment.metadata) newEnvironment.metadata = {};
|
|
842
|
+
newEnvironment.metadata.location = absoluteEnvironmentPath;
|
|
843
|
+
if (environment.materializationStorage !== undefined) {
|
|
844
|
+
newEnvironment.metadata.materializationStorage =
|
|
845
|
+
environment.materializationStorage;
|
|
813
846
|
}
|
|
814
847
|
|
|
815
|
-
this.
|
|
848
|
+
this.environments.set(environmentName, newEnvironment);
|
|
816
849
|
|
|
817
|
-
|
|
850
|
+
environment?.packages?.forEach((_package) => {
|
|
818
851
|
if (_package.name) {
|
|
819
|
-
|
|
852
|
+
newEnvironment.setPackageStatus(
|
|
853
|
+
_package.name,
|
|
854
|
+
PackageStatus.SERVING,
|
|
855
|
+
);
|
|
820
856
|
}
|
|
821
857
|
});
|
|
822
858
|
|
|
823
|
-
await this.
|
|
859
|
+
await this.addEnvironmentToDatabase(newEnvironment);
|
|
824
860
|
|
|
825
|
-
return
|
|
861
|
+
return newEnvironment;
|
|
826
862
|
}
|
|
827
863
|
|
|
828
|
-
public async
|
|
864
|
+
public async unzipEnvironment(absoluteEnvironmentPath: string) {
|
|
829
865
|
logger.info(
|
|
830
|
-
`Detected zip file at "${
|
|
866
|
+
`Detected zip file at "${absoluteEnvironmentPath}". Unzipping...`,
|
|
831
867
|
);
|
|
832
|
-
const
|
|
833
|
-
|
|
868
|
+
const unzippedEnvironmentPath = absoluteEnvironmentPath.replace(
|
|
869
|
+
".zip",
|
|
870
|
+
"",
|
|
871
|
+
);
|
|
872
|
+
await fs.promises.rm(unzippedEnvironmentPath, {
|
|
834
873
|
recursive: true,
|
|
835
874
|
force: true,
|
|
836
875
|
});
|
|
837
|
-
await fs.promises.mkdir(
|
|
876
|
+
await fs.promises.mkdir(unzippedEnvironmentPath, { recursive: true });
|
|
838
877
|
|
|
839
|
-
const zip = new AdmZip(
|
|
840
|
-
zip.extractAllTo(
|
|
878
|
+
const zip = new AdmZip(absoluteEnvironmentPath);
|
|
879
|
+
zip.extractAllTo(unzippedEnvironmentPath, true);
|
|
841
880
|
|
|
842
|
-
return
|
|
881
|
+
return unzippedEnvironmentPath;
|
|
843
882
|
}
|
|
844
883
|
|
|
845
|
-
public async
|
|
884
|
+
public async updateEnvironment(environment: ApiEnvironment) {
|
|
846
885
|
await this.finishedInitialization;
|
|
847
886
|
if (this.publisherConfigIsFrozen) {
|
|
848
887
|
throw new FrozenConfigError();
|
|
849
888
|
}
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
if (!
|
|
853
|
-
throw new Error("
|
|
889
|
+
validateEnvironmentAzureUrls(environment);
|
|
890
|
+
const environmentName = environment.name;
|
|
891
|
+
if (!environmentName) {
|
|
892
|
+
throw new Error("Environment name is required");
|
|
854
893
|
}
|
|
855
|
-
const
|
|
856
|
-
if (!
|
|
857
|
-
throw new
|
|
894
|
+
const existingEnvironment = this.environments.get(environmentName);
|
|
895
|
+
if (!existingEnvironment) {
|
|
896
|
+
throw new EnvironmentNotFoundError(
|
|
897
|
+
`Environment ${environmentName} not found`,
|
|
898
|
+
);
|
|
858
899
|
}
|
|
859
|
-
const
|
|
860
|
-
this.
|
|
861
|
-
await this.
|
|
862
|
-
return
|
|
900
|
+
const updatedEnvironment = await existingEnvironment.update(environment);
|
|
901
|
+
this.environments.set(environmentName, updatedEnvironment);
|
|
902
|
+
await this.addEnvironmentToDatabase(updatedEnvironment);
|
|
903
|
+
return updatedEnvironment;
|
|
863
904
|
}
|
|
864
905
|
|
|
865
|
-
public async
|
|
866
|
-
|
|
867
|
-
): Promise<
|
|
906
|
+
public async deleteEnvironment(
|
|
907
|
+
environmentName: string,
|
|
908
|
+
): Promise<Environment | undefined> {
|
|
868
909
|
await this.finishedInitialization;
|
|
869
910
|
if (this.publisherConfigIsFrozen) {
|
|
870
911
|
throw new FrozenConfigError();
|
|
871
912
|
}
|
|
872
|
-
const
|
|
873
|
-
if (!
|
|
913
|
+
const environment = this.environments.get(environmentName);
|
|
914
|
+
if (!environment) {
|
|
874
915
|
return;
|
|
875
916
|
}
|
|
876
917
|
|
|
877
|
-
const
|
|
918
|
+
const environmentPath = environment.metadata?.location;
|
|
878
919
|
|
|
879
|
-
// Close all connections before removing the
|
|
880
|
-
await
|
|
920
|
+
// Close all connections before removing the environment
|
|
921
|
+
await environment.closeAllConnections();
|
|
881
922
|
|
|
882
|
-
this.
|
|
883
|
-
await this.
|
|
884
|
-
if (
|
|
923
|
+
this.environments.delete(environmentName);
|
|
924
|
+
await this.deleteEnvironmentFromDatabase(environmentName);
|
|
925
|
+
if (environmentPath) {
|
|
885
926
|
try {
|
|
886
|
-
await fs.promises.rm(
|
|
887
|
-
|
|
927
|
+
await fs.promises.rm(environmentPath, {
|
|
928
|
+
recursive: true,
|
|
929
|
+
force: true,
|
|
930
|
+
});
|
|
931
|
+
logger.info(`Deleted environment directory: ${environmentPath}`);
|
|
888
932
|
} catch (err) {
|
|
889
|
-
logger.error("Error removing
|
|
933
|
+
logger.error("Error removing environment directory", {
|
|
934
|
+
error: err,
|
|
935
|
+
});
|
|
890
936
|
}
|
|
891
937
|
}
|
|
892
938
|
|
|
893
|
-
return
|
|
939
|
+
return environment;
|
|
894
940
|
}
|
|
895
941
|
|
|
896
|
-
public static async
|
|
942
|
+
public static async reloadEnvironmentManifest(
|
|
897
943
|
serverRootPath: string,
|
|
898
944
|
): Promise<ProcessedPublisherConfig> {
|
|
899
945
|
try {
|
|
@@ -904,17 +950,17 @@ export class ProjectStore {
|
|
|
904
950
|
`Error reading ${PUBLISHER_CONFIG_NAME}. Generating from directory`,
|
|
905
951
|
{ error },
|
|
906
952
|
);
|
|
907
|
-
return { frozenConfig: false,
|
|
953
|
+
return { frozenConfig: false, environments: [] };
|
|
908
954
|
} else {
|
|
909
955
|
// If publisher.config.json is missing, generate the manifest from directories
|
|
910
956
|
try {
|
|
911
957
|
const entries = await fs.promises.readdir(serverRootPath, {
|
|
912
958
|
withFileTypes: true,
|
|
913
959
|
});
|
|
914
|
-
const
|
|
960
|
+
const environments: ProcessedEnvironment[] = [];
|
|
915
961
|
for (const entry of entries) {
|
|
916
962
|
if (entry.isDirectory()) {
|
|
917
|
-
|
|
963
|
+
environments.push({
|
|
918
964
|
name: entry.name,
|
|
919
965
|
packages: [
|
|
920
966
|
{
|
|
@@ -926,31 +972,31 @@ export class ProjectStore {
|
|
|
926
972
|
});
|
|
927
973
|
}
|
|
928
974
|
}
|
|
929
|
-
return { frozenConfig: false,
|
|
975
|
+
return { frozenConfig: false, environments };
|
|
930
976
|
} catch (lsError) {
|
|
931
977
|
logger.error(`Error listing directories in ${serverRootPath}`, {
|
|
932
978
|
error: lsError,
|
|
933
979
|
});
|
|
934
|
-
return { frozenConfig: false,
|
|
980
|
+
return { frozenConfig: false, environments: [] };
|
|
935
981
|
}
|
|
936
982
|
}
|
|
937
983
|
}
|
|
938
984
|
}
|
|
939
985
|
|
|
940
|
-
private async
|
|
941
|
-
const
|
|
942
|
-
if (!
|
|
943
|
-
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");
|
|
944
990
|
}
|
|
945
|
-
const
|
|
946
|
-
await fs.promises.mkdir(
|
|
947
|
-
if (
|
|
991
|
+
const absoluteEnvironmentPath = `${this.serverRootPath}/${PUBLISHER_DATA_DIR}/${environmentName}`;
|
|
992
|
+
await fs.promises.mkdir(absoluteEnvironmentPath, { recursive: true });
|
|
993
|
+
if (environment.readme) {
|
|
948
994
|
await fs.promises.writeFile(
|
|
949
|
-
path.join(
|
|
950
|
-
|
|
995
|
+
path.join(absoluteEnvironmentPath, "README.md"),
|
|
996
|
+
environment.readme,
|
|
951
997
|
);
|
|
952
998
|
}
|
|
953
|
-
return
|
|
999
|
+
return absoluteEnvironmentPath;
|
|
954
1000
|
}
|
|
955
1001
|
|
|
956
1002
|
private isLocalPath(location: string) {
|
|
@@ -978,17 +1024,17 @@ export class ProjectStore {
|
|
|
978
1024
|
return location.startsWith("s3://");
|
|
979
1025
|
}
|
|
980
1026
|
|
|
981
|
-
private async
|
|
982
|
-
|
|
983
|
-
packages:
|
|
1027
|
+
private async loadEnvironmentIntoDisk(
|
|
1028
|
+
environmentName: string,
|
|
1029
|
+
packages: ApiEnvironment["packages"],
|
|
984
1030
|
) {
|
|
985
|
-
const absoluteTargetPath = `${this.serverRootPath}/${PUBLISHER_DATA_DIR}/${
|
|
1031
|
+
const absoluteTargetPath = `${this.serverRootPath}/${PUBLISHER_DATA_DIR}/${environmentName}`;
|
|
986
1032
|
|
|
987
1033
|
await fs.promises.mkdir(absoluteTargetPath, { recursive: true });
|
|
988
1034
|
|
|
989
1035
|
if (!packages || packages.length === 0) {
|
|
990
1036
|
throw new PackageNotFoundError(
|
|
991
|
-
`No packages found for
|
|
1037
|
+
`No packages found for environment ${environmentName}`,
|
|
992
1038
|
);
|
|
993
1039
|
}
|
|
994
1040
|
|
|
@@ -1045,7 +1091,7 @@ export class ProjectStore {
|
|
|
1045
1091
|
await this.downloadOrMountLocation(
|
|
1046
1092
|
groupedLocation,
|
|
1047
1093
|
tempDownloadPath,
|
|
1048
|
-
|
|
1094
|
+
environmentName,
|
|
1049
1095
|
"shared",
|
|
1050
1096
|
);
|
|
1051
1097
|
// Extract each package from the downloaded content
|
|
@@ -1144,7 +1190,7 @@ export class ProjectStore {
|
|
|
1144
1190
|
private async downloadOrMountLocation(
|
|
1145
1191
|
location: string,
|
|
1146
1192
|
targetPath: string,
|
|
1147
|
-
|
|
1193
|
+
environmentName: string,
|
|
1148
1194
|
packageName: string,
|
|
1149
1195
|
) {
|
|
1150
1196
|
const isCompressedFile = location.endsWith(".zip");
|
|
@@ -1156,7 +1202,7 @@ export class ProjectStore {
|
|
|
1156
1202
|
);
|
|
1157
1203
|
await this.downloadGcsDirectory(
|
|
1158
1204
|
location,
|
|
1159
|
-
|
|
1205
|
+
environmentName,
|
|
1160
1206
|
targetPath,
|
|
1161
1207
|
isCompressedFile,
|
|
1162
1208
|
);
|
|
@@ -1201,7 +1247,7 @@ export class ProjectStore {
|
|
|
1201
1247
|
);
|
|
1202
1248
|
await this.downloadS3Directory(
|
|
1203
1249
|
location,
|
|
1204
|
-
|
|
1250
|
+
environmentName,
|
|
1205
1251
|
targetPath,
|
|
1206
1252
|
isCompressedFile,
|
|
1207
1253
|
);
|
|
@@ -1230,7 +1276,7 @@ export class ProjectStore {
|
|
|
1230
1276
|
await this.mountLocalDirectory(
|
|
1231
1277
|
packagePath,
|
|
1232
1278
|
targetPath,
|
|
1233
|
-
|
|
1279
|
+
environmentName,
|
|
1234
1280
|
packageName,
|
|
1235
1281
|
);
|
|
1236
1282
|
return;
|
|
@@ -1248,40 +1294,40 @@ export class ProjectStore {
|
|
|
1248
1294
|
|
|
1249
1295
|
// If we get here, the path format is not supported
|
|
1250
1296
|
const errorMsg = `Invalid package path: "${location}". Must be an absolute mounted path or a GCS/S3/GitHub URI.`;
|
|
1251
|
-
logger.error(errorMsg, {
|
|
1297
|
+
logger.error(errorMsg, { environmentName, location });
|
|
1252
1298
|
throw new PackageNotFoundError(errorMsg);
|
|
1253
1299
|
}
|
|
1254
1300
|
|
|
1255
1301
|
public async mountLocalDirectory(
|
|
1256
|
-
|
|
1302
|
+
environmentPath: string,
|
|
1257
1303
|
absoluteTargetPath: string,
|
|
1258
|
-
|
|
1304
|
+
environmentName: string,
|
|
1259
1305
|
packageName: string,
|
|
1260
1306
|
) {
|
|
1261
|
-
if (
|
|
1262
|
-
|
|
1307
|
+
if (environmentPath.endsWith(".zip")) {
|
|
1308
|
+
environmentPath = await this.unzipEnvironment(environmentPath);
|
|
1263
1309
|
}
|
|
1264
|
-
const
|
|
1265
|
-
(await fs.promises.stat(
|
|
1266
|
-
if (
|
|
1310
|
+
const environmentDirExists =
|
|
1311
|
+
(await fs.promises.stat(environmentPath))?.isDirectory() ?? false;
|
|
1312
|
+
if (environmentDirExists) {
|
|
1267
1313
|
await fs.promises.rm(absoluteTargetPath, {
|
|
1268
1314
|
recursive: true,
|
|
1269
1315
|
force: true,
|
|
1270
1316
|
});
|
|
1271
1317
|
await fs.promises.mkdir(absoluteTargetPath, { recursive: true });
|
|
1272
|
-
await fs.promises.cp(
|
|
1318
|
+
await fs.promises.cp(environmentPath, absoluteTargetPath, {
|
|
1273
1319
|
recursive: true,
|
|
1274
1320
|
});
|
|
1275
1321
|
} else {
|
|
1276
1322
|
throw new PackageNotFoundError(
|
|
1277
|
-
`Package ${packageName} for
|
|
1323
|
+
`Package ${packageName} for environment ${environmentName} not found in "${environmentPath}"`,
|
|
1278
1324
|
);
|
|
1279
1325
|
}
|
|
1280
1326
|
}
|
|
1281
1327
|
|
|
1282
1328
|
async downloadGcsDirectory(
|
|
1283
1329
|
gcsPath: string,
|
|
1284
|
-
|
|
1330
|
+
environmentName: string,
|
|
1285
1331
|
absoluteDirPath: string,
|
|
1286
1332
|
isCompressedFile: boolean,
|
|
1287
1333
|
) {
|
|
@@ -1292,8 +1338,8 @@ export class ProjectStore {
|
|
|
1292
1338
|
prefix,
|
|
1293
1339
|
});
|
|
1294
1340
|
if (files.length === 0) {
|
|
1295
|
-
throw new
|
|
1296
|
-
`
|
|
1341
|
+
throw new EnvironmentNotFoundError(
|
|
1342
|
+
`Environment ${environmentName} not found in ${gcsPath}`,
|
|
1297
1343
|
);
|
|
1298
1344
|
}
|
|
1299
1345
|
if (!isCompressedFile) {
|
|
@@ -1324,14 +1370,14 @@ export class ProjectStore {
|
|
|
1324
1370
|
}),
|
|
1325
1371
|
);
|
|
1326
1372
|
if (isCompressedFile) {
|
|
1327
|
-
await this.
|
|
1373
|
+
await this.unzipEnvironment(absoluteDirPath);
|
|
1328
1374
|
}
|
|
1329
1375
|
logger.info(`Downloaded GCS directory ${gcsPath} to ${absoluteDirPath}`);
|
|
1330
1376
|
}
|
|
1331
1377
|
|
|
1332
1378
|
async downloadS3Directory(
|
|
1333
1379
|
s3Path: string,
|
|
1334
|
-
|
|
1380
|
+
environmentName: string,
|
|
1335
1381
|
absoluteDirPath: string,
|
|
1336
1382
|
isCompressedFile: boolean = false,
|
|
1337
1383
|
) {
|
|
@@ -1352,8 +1398,8 @@ export class ProjectStore {
|
|
|
1352
1398
|
});
|
|
1353
1399
|
const item = await this.s3Client.send(command);
|
|
1354
1400
|
if (!item.Body) {
|
|
1355
|
-
throw new
|
|
1356
|
-
`
|
|
1401
|
+
throw new EnvironmentNotFoundError(
|
|
1402
|
+
`Environment ${environmentName} not found in ${s3Path}`,
|
|
1357
1403
|
);
|
|
1358
1404
|
}
|
|
1359
1405
|
const file = fs.createWriteStream(zipFilePath);
|
|
@@ -1364,7 +1410,7 @@ export class ProjectStore {
|
|
|
1364
1410
|
});
|
|
1365
1411
|
|
|
1366
1412
|
// Extract the zip file
|
|
1367
|
-
await this.
|
|
1413
|
+
await this.unzipEnvironment(zipFilePath);
|
|
1368
1414
|
logger.info(`Downloaded S3 zip file ${s3Path} to ${absoluteDirPath}`);
|
|
1369
1415
|
return;
|
|
1370
1416
|
}
|
|
@@ -1378,8 +1424,8 @@ export class ProjectStore {
|
|
|
1378
1424
|
await fs.promises.mkdir(absoluteDirPath, { recursive: true });
|
|
1379
1425
|
|
|
1380
1426
|
if (!objects.Contents || objects.Contents.length === 0) {
|
|
1381
|
-
throw new
|
|
1382
|
-
`
|
|
1427
|
+
throw new EnvironmentNotFoundError(
|
|
1428
|
+
`Environment ${environmentName} not found in ${s3Path}`,
|
|
1383
1429
|
);
|
|
1384
1430
|
}
|
|
1385
1431
|
await Promise.all(
|