@malloy-publisher/server 0.0.197-dev → 0.0.198-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1139 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /**
3
+ * Legacy `/projects/...` route registration.
4
+ *
5
+ * The publisher's API was renamed from `projects` to `environments`. This module
6
+ * registers the old `/projects/...` paths on the same Express app so existing
7
+ * SDK clients (e.g. `@malloydata/db-publisher`) keep working without code
8
+ * changes on their side.
9
+ *
10
+ * Implementation strategy:
11
+ * - Reuse the same controllers that `server.ts` wires up; only the URL
12
+ * surface changes.
13
+ * - Most response models (`Connection`, `Package`, `Model`, `Notebook`,
14
+ * `Database`, `Table`, `QueryResult`, etc.) have identical JSON wire
15
+ * format between old (`Project`) and new (`Environment`) specs, so they
16
+ * pass through unchanged.
17
+ * - The handful of payloads that DO have field-level renames are remapped:
18
+ * * GET /status — `environments` -> `projects`
19
+ * * Materialization responses — `environmentId` -> `projectId`
20
+ *
21
+ * - Watch-mode is intentionally not exposed under the legacy prefix; clients
22
+ * that need it should use the new `/environments/...` paths directly.
23
+ */
24
+
25
+ import bodyParser from "body-parser";
26
+ import type { Express, Response } from "express";
27
+ import { ParsedQs } from "qs";
28
+ import { CompileController } from "./controller/compile.controller";
29
+ import { ConnectionController } from "./controller/connection.controller";
30
+ import { DatabaseController } from "./controller/database.controller";
31
+ import { ManifestController } from "./controller/manifest.controller";
32
+ import { MaterializationController } from "./controller/materialization.controller";
33
+ import { ModelController } from "./controller/model.controller";
34
+ import { PackageController } from "./controller/package.controller";
35
+ import { QueryController } from "./controller/query.controller";
36
+ import {
37
+ BadRequestError,
38
+ internalErrorToHttpError,
39
+ NotImplementedError,
40
+ } from "./errors";
41
+ import { logger } from "./logger";
42
+ import { normalizeQueryArray } from "./server";
43
+ import { EnvironmentStore } from "./service/environment_store";
44
+
45
+ const LEGACY_API_PREFIX = "/api/v0";
46
+
47
+ /** Bag of controllers shared with the new server. */
48
+ export interface LegacyControllerSet {
49
+ environmentStore: EnvironmentStore;
50
+ connectionController: ConnectionController;
51
+ modelController: ModelController;
52
+ packageController: PackageController;
53
+ databaseController: DatabaseController;
54
+ queryController: QueryController;
55
+ compileController: CompileController;
56
+ materializationController: MaterializationController;
57
+ manifestController: ManifestController;
58
+ }
59
+
60
+ // ─── response/body field mappers ───────────────────────────────────────────
61
+
62
+ function remapStatusResponse(status: any): any {
63
+ if (!status || typeof status !== "object") return status;
64
+ const out: Record<string, any> = { ...status };
65
+ if ("environments" in out) {
66
+ out.projects = out.environments;
67
+ delete out.environments;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ function remapMaterializationResponse(mat: any): any {
73
+ if (!mat || typeof mat !== "object") return mat;
74
+ if (Array.isArray(mat)) {
75
+ return mat.map(remapMaterializationResponse);
76
+ }
77
+ const out: Record<string, any> = { ...mat };
78
+ if ("environmentId" in out) {
79
+ out.projectId = out.environmentId;
80
+ delete out.environmentId;
81
+ }
82
+ return out;
83
+ }
84
+
85
+ const setVersionIdError = (res: Response) => {
86
+ const { json, status } = internalErrorToHttpError(
87
+ new NotImplementedError("Version IDs not implemented."),
88
+ );
89
+ res.status(status).json(json);
90
+ };
91
+
92
+ // ─── route registration ────────────────────────────────────────────────────
93
+
94
+ export function registerLegacyRoutes(
95
+ app: Express,
96
+ controllers: LegacyControllerSet,
97
+ ) {
98
+ const {
99
+ environmentStore,
100
+ connectionController,
101
+ modelController,
102
+ packageController,
103
+ databaseController,
104
+ queryController,
105
+ compileController,
106
+ materializationController,
107
+ manifestController,
108
+ } = controllers;
109
+
110
+ // body-parser is already registered on the main app for `${API_PREFIX}/*`
111
+ // paths via `app.use(bodyParser.json(...))`. The legacy routes share the
112
+ // same `${API_PREFIX}` prefix so they inherit it automatically.
113
+ void bodyParser; // keep the import; helper file reference for clarity
114
+
115
+ // ── status ──────────────────────────────────────────────────────────────
116
+ app.get(`${LEGACY_API_PREFIX}/status`, async (_req, res) => {
117
+ try {
118
+ const status = await environmentStore.getStatus();
119
+ res.status(200).json(remapStatusResponse(status));
120
+ } catch (error) {
121
+ logger.error("Error getting status", { error });
122
+ const { json, status } = internalErrorToHttpError(error as Error);
123
+ res.status(status).json(json);
124
+ }
125
+ });
126
+
127
+ // ── projects (== environments) ──────────────────────────────────────────
128
+ app.get(`${LEGACY_API_PREFIX}/projects`, async (_req, res) => {
129
+ try {
130
+ res.status(200).json(await environmentStore.listEnvironments());
131
+ } catch (error) {
132
+ logger.error(error);
133
+ const { json, status } = internalErrorToHttpError(error as Error);
134
+ res.status(status).json(json);
135
+ }
136
+ });
137
+
138
+ app.post(`${LEGACY_API_PREFIX}/projects`, async (req, res) => {
139
+ try {
140
+ logger.info("Adding project", { body: req.body });
141
+ const environment = await environmentStore.addEnvironment(req.body);
142
+ res.status(200).json(await environment.serialize());
143
+ } catch (error) {
144
+ logger.error(error);
145
+ const { json, status } = internalErrorToHttpError(error as Error);
146
+ res.status(status).json(json);
147
+ }
148
+ });
149
+
150
+ app.get(`${LEGACY_API_PREFIX}/projects/:projectName`, async (req, res) => {
151
+ try {
152
+ const environment = await environmentStore.getEnvironment(
153
+ req.params.projectName,
154
+ req.query.reload === "true",
155
+ );
156
+ res.status(200).json(await environment.serialize());
157
+ } catch (error) {
158
+ logger.error(error);
159
+ const { json, status } = internalErrorToHttpError(error as Error);
160
+ res.status(status).json(json);
161
+ }
162
+ });
163
+
164
+ app.patch(`${LEGACY_API_PREFIX}/projects/:projectName`, async (req, res) => {
165
+ try {
166
+ const environment = await environmentStore.updateEnvironment(req.body);
167
+ res.status(200).json(await environment.serialize());
168
+ } catch (error) {
169
+ logger.error(error);
170
+ const { json, status } = internalErrorToHttpError(error as Error);
171
+ res.status(status).json(json);
172
+ }
173
+ });
174
+
175
+ app.delete(
176
+ `${LEGACY_API_PREFIX}/projects/:projectName`,
177
+ async (req, res) => {
178
+ try {
179
+ const environment = await environmentStore.deleteEnvironment(
180
+ req.params.projectName,
181
+ );
182
+ res.status(200).json(await environment?.serialize());
183
+ } catch (error) {
184
+ logger.error(error);
185
+ const { json, status } = internalErrorToHttpError(error as Error);
186
+ res.status(status).json(json);
187
+ }
188
+ },
189
+ );
190
+
191
+ // ── connections ─────────────────────────────────────────────────────────
192
+ app.get(
193
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections`,
194
+ async (req, res) => {
195
+ try {
196
+ res.status(200).json(
197
+ await connectionController.listConnections(
198
+ req.params.projectName,
199
+ ),
200
+ );
201
+ } catch (error) {
202
+ logger.error(error);
203
+ const { json, status } = internalErrorToHttpError(error as Error);
204
+ res.status(status).json(json);
205
+ }
206
+ },
207
+ );
208
+
209
+ app.get(
210
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName`,
211
+ async (req, res) => {
212
+ try {
213
+ res.status(200).json(
214
+ await connectionController.getConnection(
215
+ req.params.projectName,
216
+ req.params.connectionName,
217
+ ),
218
+ );
219
+ } catch (error) {
220
+ logger.error(error);
221
+ const { json, status } = internalErrorToHttpError(error as Error);
222
+ res.status(status).json(json);
223
+ }
224
+ },
225
+ );
226
+
227
+ app.post(
228
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName`,
229
+ async (req, res) => {
230
+ try {
231
+ const result = await connectionController.addConnection(
232
+ req.params.projectName,
233
+ req.params.connectionName,
234
+ req.body,
235
+ );
236
+ res.status(201).json(result);
237
+ } catch (error) {
238
+ logger.error("Error creating connection", { error });
239
+ const { json, status } = internalErrorToHttpError(error as Error);
240
+ res.status(status).json(json);
241
+ }
242
+ },
243
+ );
244
+
245
+ app.patch(
246
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName`,
247
+ async (req, res) => {
248
+ try {
249
+ const result = await connectionController.updateConnection(
250
+ req.params.projectName,
251
+ req.params.connectionName,
252
+ req.body,
253
+ );
254
+ res.status(200).json(result);
255
+ } catch (error) {
256
+ logger.error("Error updating connection", { error });
257
+ const { json, status } = internalErrorToHttpError(error as Error);
258
+ res.status(status).json(json);
259
+ }
260
+ },
261
+ );
262
+
263
+ app.delete(
264
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName`,
265
+ async (req, res) => {
266
+ try {
267
+ const result = await connectionController.deleteConnection(
268
+ req.params.projectName,
269
+ req.params.connectionName,
270
+ );
271
+ res.status(200).json(result);
272
+ } catch (error) {
273
+ logger.error("Error deleting connection", { error });
274
+ const { json, status } = internalErrorToHttpError(error as Error);
275
+ res.status(status).json(json);
276
+ }
277
+ },
278
+ );
279
+
280
+ // /connections/test is org-level (no projectName) and unchanged between
281
+ // old and new specs — it's already registered on the main app.
282
+
283
+ app.get(
284
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/schemas`,
285
+ async (req, res) => {
286
+ try {
287
+ res.status(200).json(
288
+ await connectionController.listSchemas(
289
+ req.params.projectName,
290
+ req.params.connectionName,
291
+ ),
292
+ );
293
+ } catch (error) {
294
+ logger.error(error);
295
+ const { json, status } = internalErrorToHttpError(error as Error);
296
+ res.status(status).json(json);
297
+ }
298
+ },
299
+ );
300
+
301
+ app.get(
302
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/schemas/:schemaName/tables`,
303
+ async (req, res) => {
304
+ try {
305
+ const results = await connectionController.listTables(
306
+ req.params.projectName,
307
+ req.params.connectionName,
308
+ req.params.schemaName,
309
+ normalizeQueryArray(req.query.tableNames),
310
+ );
311
+ res.status(200).json(results);
312
+ } catch (error) {
313
+ logger.error(error);
314
+ const { json, status } = internalErrorToHttpError(error as Error);
315
+ res.status(status).json(json);
316
+ }
317
+ },
318
+ );
319
+
320
+ app.get(
321
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/schemas/:schemaName/tables/:tablePath`,
322
+ async (req, res) => {
323
+ try {
324
+ const results = await connectionController.getTable(
325
+ req.params.projectName,
326
+ req.params.connectionName,
327
+ req.params.schemaName,
328
+ req.params.tablePath,
329
+ );
330
+ res.status(200).json(results);
331
+ } catch (error) {
332
+ logger.error(error);
333
+ const { json, status } = internalErrorToHttpError(error as Error);
334
+ res.status(status).json(json);
335
+ }
336
+ },
337
+ );
338
+
339
+ // Per-package connection routes (duckdb context)
340
+ app.get(
341
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/schemas`,
342
+ async (req, res) => {
343
+ try {
344
+ res.status(200).json(
345
+ await connectionController.listSchemas(
346
+ req.params.projectName,
347
+ req.params.connectionName,
348
+ req.params.packageName,
349
+ ),
350
+ );
351
+ } catch (error) {
352
+ logger.error(error);
353
+ const { json, status } = internalErrorToHttpError(error as Error);
354
+ res.status(status).json(json);
355
+ }
356
+ },
357
+ );
358
+
359
+ app.get(
360
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/schemas/:schemaName/tables`,
361
+ async (req, res) => {
362
+ try {
363
+ res.status(200).json(
364
+ await connectionController.listTables(
365
+ req.params.projectName,
366
+ req.params.connectionName,
367
+ req.params.schemaName,
368
+ normalizeQueryArray(req.query.tableNames),
369
+ req.params.packageName,
370
+ ),
371
+ );
372
+ } catch (error) {
373
+ logger.error(error);
374
+ const { json, status } = internalErrorToHttpError(error as Error);
375
+ res.status(status).json(json);
376
+ }
377
+ },
378
+ );
379
+
380
+ app.get(
381
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/schemas/:schemaName/tables/:tablePath`,
382
+ async (req, res) => {
383
+ try {
384
+ res.status(200).json(
385
+ await connectionController.getTable(
386
+ req.params.projectName,
387
+ req.params.connectionName,
388
+ req.params.schemaName,
389
+ req.params.tablePath,
390
+ req.params.packageName,
391
+ ),
392
+ );
393
+ } catch (error) {
394
+ logger.error(error);
395
+ const { json, status } = internalErrorToHttpError(error as Error);
396
+ res.status(status).json(json);
397
+ }
398
+ },
399
+ );
400
+
401
+ // sqlSource (deprecated GET + supported POST), per-project + per-package
402
+ app.get(
403
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlSource`,
404
+ async (req, res) => {
405
+ try {
406
+ res.status(200).json(
407
+ await connectionController.getConnectionSqlSource(
408
+ req.params.projectName,
409
+ req.params.connectionName,
410
+ req.query.sqlStatement as string,
411
+ ),
412
+ );
413
+ } catch (error) {
414
+ logger.error(error);
415
+ const { json, status } = internalErrorToHttpError(error as Error);
416
+ res.status(status).json(json);
417
+ }
418
+ },
419
+ );
420
+
421
+ app.post(
422
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlSource`,
423
+ async (req, res) => {
424
+ try {
425
+ res.status(200).json(
426
+ await connectionController.getConnectionSqlSource(
427
+ req.params.projectName,
428
+ req.params.connectionName,
429
+ req.body.sqlStatement as string,
430
+ ),
431
+ );
432
+ } catch (error) {
433
+ logger.error(error);
434
+ const { json, status } = internalErrorToHttpError(error as Error);
435
+ res.status(status).json(json);
436
+ }
437
+ },
438
+ );
439
+
440
+ app.get(
441
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlSource`,
442
+ async (req, res) => {
443
+ try {
444
+ res.status(200).json(
445
+ await connectionController.getConnectionSqlSource(
446
+ req.params.projectName,
447
+ req.params.connectionName,
448
+ req.query.sqlStatement as string,
449
+ req.params.packageName,
450
+ ),
451
+ );
452
+ } catch (error) {
453
+ logger.error(error);
454
+ const { json, status } = internalErrorToHttpError(error as Error);
455
+ res.status(status).json(json);
456
+ }
457
+ },
458
+ );
459
+
460
+ app.post(
461
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlSource`,
462
+ async (req, res) => {
463
+ try {
464
+ res.status(200).json(
465
+ await connectionController.getConnectionSqlSource(
466
+ req.params.projectName,
467
+ req.params.connectionName,
468
+ req.body.sqlStatement as string,
469
+ req.params.packageName,
470
+ ),
471
+ );
472
+ } catch (error) {
473
+ logger.error(error);
474
+ const { json, status } = internalErrorToHttpError(error as Error);
475
+ res.status(status).json(json);
476
+ }
477
+ },
478
+ );
479
+
480
+ // queryData (deprecated GET) + sqlQuery (supported POST), per-project +
481
+ // per-package
482
+ app.get(
483
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/queryData`,
484
+ async (req, res) => {
485
+ try {
486
+ res.status(200).json(
487
+ await connectionController.getConnectionQueryData(
488
+ req.params.projectName,
489
+ req.params.connectionName,
490
+ req.query.sqlStatement as string,
491
+ req.query.options as string,
492
+ ),
493
+ );
494
+ } catch (error) {
495
+ logger.error(error);
496
+ const { json, status } = internalErrorToHttpError(error as Error);
497
+ res.status(status).json(json);
498
+ }
499
+ },
500
+ );
501
+
502
+ app.get(
503
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/queryData`,
504
+ async (req, res) => {
505
+ try {
506
+ res.status(200).json(
507
+ await connectionController.getConnectionQueryData(
508
+ req.params.projectName,
509
+ req.params.connectionName,
510
+ req.query.sqlStatement as string,
511
+ req.query.options as string,
512
+ req.params.packageName,
513
+ ),
514
+ );
515
+ } catch (error) {
516
+ logger.error(error);
517
+ const { json, status } = internalErrorToHttpError(error as Error);
518
+ res.status(status).json(json);
519
+ }
520
+ },
521
+ );
522
+
523
+ app.post(
524
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlQuery`,
525
+ async (req, res) => {
526
+ try {
527
+ let options: string | ParsedQs | (string | ParsedQs)[] | undefined;
528
+ if (req.body?.options) {
529
+ options = req.body.options;
530
+ } else {
531
+ options = req.query.options;
532
+ }
533
+ res.status(200).json(
534
+ await connectionController.getConnectionQueryData(
535
+ req.params.projectName,
536
+ req.params.connectionName,
537
+ req.body.sqlStatement as string,
538
+ options as string,
539
+ ),
540
+ );
541
+ } catch (error) {
542
+ logger.error(error);
543
+ const { json, status } = internalErrorToHttpError(error as Error);
544
+ res.status(status).json(json);
545
+ }
546
+ },
547
+ );
548
+
549
+ app.post(
550
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlQuery`,
551
+ async (req, res) => {
552
+ try {
553
+ let options: string | ParsedQs | (string | ParsedQs)[] | undefined;
554
+ if (req.body?.options) {
555
+ options = req.body.options;
556
+ } else {
557
+ options = req.query.options;
558
+ }
559
+ res.status(200).json(
560
+ await connectionController.getConnectionQueryData(
561
+ req.params.projectName,
562
+ req.params.connectionName,
563
+ req.body.sqlStatement as string,
564
+ options as string,
565
+ req.params.packageName,
566
+ ),
567
+ );
568
+ } catch (error) {
569
+ logger.error(error);
570
+ const { json, status } = internalErrorToHttpError(error as Error);
571
+ res.status(status).json(json);
572
+ }
573
+ },
574
+ );
575
+
576
+ // temporaryTable (deprecated GET) + sqlTemporaryTable (supported POST)
577
+ app.get(
578
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/temporaryTable`,
579
+ async (req, res) => {
580
+ try {
581
+ res.status(200).json(
582
+ await connectionController.getConnectionTemporaryTable(
583
+ req.params.projectName,
584
+ req.params.connectionName,
585
+ req.query.sqlStatement as string,
586
+ ),
587
+ );
588
+ } catch (error) {
589
+ logger.error(error);
590
+ const { json, status } = internalErrorToHttpError(error as Error);
591
+ res.status(status).json(json);
592
+ }
593
+ },
594
+ );
595
+
596
+ app.get(
597
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/temporaryTable`,
598
+ async (req, res) => {
599
+ try {
600
+ res.status(200).json(
601
+ await connectionController.getConnectionTemporaryTable(
602
+ req.params.projectName,
603
+ req.params.connectionName,
604
+ req.query.sqlStatement as string,
605
+ req.params.packageName,
606
+ ),
607
+ );
608
+ } catch (error) {
609
+ logger.error(error);
610
+ const { json, status } = internalErrorToHttpError(error as Error);
611
+ res.status(status).json(json);
612
+ }
613
+ },
614
+ );
615
+
616
+ app.post(
617
+ `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlTemporaryTable`,
618
+ async (req, res) => {
619
+ try {
620
+ res.status(200).json(
621
+ await connectionController.getConnectionTemporaryTable(
622
+ req.params.projectName,
623
+ req.params.connectionName,
624
+ req.body.sqlStatement as string,
625
+ ),
626
+ );
627
+ } catch (error) {
628
+ logger.error(error);
629
+ const { json, status } = internalErrorToHttpError(error as Error);
630
+ res.status(status).json(json);
631
+ }
632
+ },
633
+ );
634
+
635
+ app.post(
636
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlTemporaryTable`,
637
+ async (req, res) => {
638
+ try {
639
+ res.status(200).json(
640
+ await connectionController.getConnectionTemporaryTable(
641
+ req.params.projectName,
642
+ req.params.connectionName,
643
+ req.body.sqlStatement as string,
644
+ req.params.packageName,
645
+ ),
646
+ );
647
+ } catch (error) {
648
+ logger.error(error);
649
+ const { json, status } = internalErrorToHttpError(error as Error);
650
+ res.status(status).json(json);
651
+ }
652
+ },
653
+ );
654
+
655
+ // ── packages ────────────────────────────────────────────────────────────
656
+ app.get(
657
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages`,
658
+ async (req, res) => {
659
+ if (req.query.versionId) {
660
+ setVersionIdError(res);
661
+ return;
662
+ }
663
+ try {
664
+ res.status(200).json(
665
+ await packageController.listPackages(req.params.projectName),
666
+ );
667
+ } catch (error) {
668
+ logger.error(error);
669
+ const { json, status } = internalErrorToHttpError(error as Error);
670
+ res.status(status).json(json);
671
+ }
672
+ },
673
+ );
674
+
675
+ app.post(
676
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages`,
677
+ async (req, res) => {
678
+ try {
679
+ const autoLoadManifest = req.query.autoLoadManifest === "true";
680
+ const _package = await packageController.addPackage(
681
+ req.params.projectName,
682
+ req.body,
683
+ { autoLoadManifest },
684
+ );
685
+ res.status(200).json(_package?.getPackageMetadata());
686
+ } catch (error) {
687
+ logger.error(error);
688
+ const { json, status } = internalErrorToHttpError(error as Error);
689
+ res.status(status).json(json);
690
+ }
691
+ },
692
+ );
693
+
694
+ app.get(
695
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName`,
696
+ async (req, res) => {
697
+ if (req.query.versionId) {
698
+ setVersionIdError(res);
699
+ return;
700
+ }
701
+ try {
702
+ res.status(200).json(
703
+ await packageController.getPackage(
704
+ req.params.projectName,
705
+ req.params.packageName,
706
+ req.query.reload === "true",
707
+ ),
708
+ );
709
+ } catch (error) {
710
+ logger.error(error);
711
+ const { json, status } = internalErrorToHttpError(error as Error);
712
+ res.status(status).json(json);
713
+ }
714
+ },
715
+ );
716
+
717
+ app.patch(
718
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName`,
719
+ async (req, res) => {
720
+ try {
721
+ res.status(200).json(
722
+ await packageController.updatePackage(
723
+ req.params.projectName,
724
+ req.params.packageName,
725
+ req.body,
726
+ ),
727
+ );
728
+ } catch (error) {
729
+ logger.error(error);
730
+ const { json, status } = internalErrorToHttpError(error as Error);
731
+ res.status(status).json(json);
732
+ }
733
+ },
734
+ );
735
+
736
+ app.delete(
737
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName`,
738
+ async (req, res) => {
739
+ try {
740
+ res.status(200).json(
741
+ await packageController.deletePackage(
742
+ req.params.projectName,
743
+ req.params.packageName,
744
+ ),
745
+ );
746
+ } catch (error) {
747
+ logger.error(error);
748
+ const { json, status } = internalErrorToHttpError(error as Error);
749
+ res.status(status).json(json);
750
+ }
751
+ },
752
+ );
753
+
754
+ // ── models ──────────────────────────────────────────────────────────────
755
+ app.get(
756
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/models`,
757
+ async (req, res) => {
758
+ if (req.query.versionId) {
759
+ setVersionIdError(res);
760
+ return;
761
+ }
762
+ try {
763
+ res.status(200).json(
764
+ await modelController.listModels(
765
+ req.params.projectName,
766
+ req.params.packageName,
767
+ ),
768
+ );
769
+ } catch (error) {
770
+ logger.error(error);
771
+ const { json, status } = internalErrorToHttpError(error as Error);
772
+ res.status(status).json(json);
773
+ }
774
+ },
775
+ );
776
+
777
+ app.get(
778
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/models/*?`,
779
+ async (req, res) => {
780
+ if (req.query.versionId) {
781
+ setVersionIdError(res);
782
+ return;
783
+ }
784
+ try {
785
+ const modelPath = (req.params as Record<string, string>)["0"];
786
+ res.status(200).json(
787
+ await modelController.getModel(
788
+ req.params.projectName,
789
+ req.params.packageName,
790
+ modelPath,
791
+ ),
792
+ );
793
+ } catch (error) {
794
+ logger.error(error);
795
+ const { json, status } = internalErrorToHttpError(error as Error);
796
+ res.status(status).json(json);
797
+ }
798
+ },
799
+ );
800
+
801
+ app.post(
802
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/models/*?/query`,
803
+ async (req, res) => {
804
+ if (req.body.versionId) {
805
+ setVersionIdError(res);
806
+ return;
807
+ }
808
+ try {
809
+ const modelPath = (req.params as Record<string, string>)["0"];
810
+ res.status(200).json(
811
+ await queryController.getQuery(
812
+ req.params.projectName,
813
+ req.params.packageName,
814
+ modelPath,
815
+ req.body.sourceName as string,
816
+ req.body.queryName as string,
817
+ req.body.query as string,
818
+ req.body.compactJson === true,
819
+ (req.body.filterParams ?? req.body.sourceFilters) as
820
+ | Record<string, string | string[]>
821
+ | undefined,
822
+ req.body.bypassFilters === true ? true : undefined,
823
+ ),
824
+ );
825
+ } catch (error) {
826
+ logger.error(error);
827
+ const { json, status } = internalErrorToHttpError(error as Error);
828
+ res.status(status).json(json);
829
+ }
830
+ },
831
+ );
832
+
833
+ app.post(
834
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/models/:modelName/compile`,
835
+ async (req, res) => {
836
+ try {
837
+ const result = await compileController.compile(
838
+ req.params.projectName,
839
+ req.params.packageName,
840
+ req.params.modelName,
841
+ req.body.source,
842
+ req.body.includeSql === true,
843
+ );
844
+ res.status(200).json(result);
845
+ } catch (error) {
846
+ logger.error("Compilation error", { error });
847
+ const { json, status } = internalErrorToHttpError(error as Error);
848
+ res.status(status).json(json);
849
+ }
850
+ },
851
+ );
852
+
853
+ // ── notebooks ───────────────────────────────────────────────────────────
854
+ app.get(
855
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/notebooks`,
856
+ async (req, res) => {
857
+ if (req.query.versionId) {
858
+ setVersionIdError(res);
859
+ return;
860
+ }
861
+ try {
862
+ res.status(200).json(
863
+ await modelController.listNotebooks(
864
+ req.params.projectName,
865
+ req.params.packageName,
866
+ ),
867
+ );
868
+ } catch (error) {
869
+ logger.error(error);
870
+ const { json, status } = internalErrorToHttpError(error as Error);
871
+ res.status(status).json(json);
872
+ }
873
+ },
874
+ );
875
+
876
+ // Cell execution route comes BEFORE the general getNotebook wildcard
877
+ app.get(
878
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/notebooks/*/cells/:cellIndex`,
879
+ async (req, res) => {
880
+ if (req.query.versionId) {
881
+ setVersionIdError(res);
882
+ return;
883
+ }
884
+ try {
885
+ const cellIndex = parseInt(req.params.cellIndex, 10);
886
+ if (isNaN(cellIndex)) {
887
+ res.status(400).json({ error: "Invalid cell index" });
888
+ return;
889
+ }
890
+ const notebookPath = (req.params as Record<string, string>)["0"];
891
+ let filterParams: Record<string, string | string[]> | undefined;
892
+ if (typeof req.query.filter_params === "string") {
893
+ try {
894
+ filterParams = JSON.parse(req.query.filter_params);
895
+ } catch {
896
+ res.status(400).json({
897
+ error: "Invalid filter_params: must be valid JSON",
898
+ });
899
+ return;
900
+ }
901
+ }
902
+ const bypassFilters =
903
+ req.query.bypass_filters === "true" ? true : undefined;
904
+ res.status(200).json(
905
+ await modelController.executeNotebookCell(
906
+ req.params.projectName,
907
+ req.params.packageName,
908
+ notebookPath,
909
+ cellIndex,
910
+ filterParams,
911
+ bypassFilters,
912
+ ),
913
+ );
914
+ } catch (error) {
915
+ logger.error(error);
916
+ const { json, status } = internalErrorToHttpError(error as Error);
917
+ res.status(status).json(json);
918
+ }
919
+ },
920
+ );
921
+
922
+ app.get(
923
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/notebooks/*?`,
924
+ async (req, res) => {
925
+ if (req.query.versionId) {
926
+ setVersionIdError(res);
927
+ return;
928
+ }
929
+ try {
930
+ const notebookPath = (req.params as Record<string, string>)["0"];
931
+ res.status(200).json(
932
+ await modelController.getNotebook(
933
+ req.params.projectName,
934
+ req.params.packageName,
935
+ notebookPath,
936
+ ),
937
+ );
938
+ } catch (error) {
939
+ logger.error(error);
940
+ const { json, status } = internalErrorToHttpError(error as Error);
941
+ res.status(status).json(json);
942
+ }
943
+ },
944
+ );
945
+
946
+ // ── databases ───────────────────────────────────────────────────────────
947
+ app.get(
948
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/databases`,
949
+ async (req, res) => {
950
+ if (req.query.versionId) {
951
+ setVersionIdError(res);
952
+ return;
953
+ }
954
+ try {
955
+ res.status(200).json(
956
+ await databaseController.listDatabases(
957
+ req.params.projectName,
958
+ req.params.packageName,
959
+ ),
960
+ );
961
+ } catch (error) {
962
+ logger.error(error);
963
+ const { json, status } = internalErrorToHttpError(error as Error);
964
+ res.status(status).json(json);
965
+ }
966
+ },
967
+ );
968
+
969
+ // ── materializations ────────────────────────────────────────────────────
970
+ app.post(
971
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations`,
972
+ async (req, res) => {
973
+ try {
974
+ const build = await materializationController.createMaterialization(
975
+ req.params.projectName,
976
+ req.params.packageName,
977
+ req.body || {},
978
+ );
979
+ res.status(201).json(remapMaterializationResponse(build));
980
+ } catch (error) {
981
+ const { json, status } = internalErrorToHttpError(error as Error);
982
+ res.status(status).json(json);
983
+ }
984
+ },
985
+ );
986
+
987
+ app.get(
988
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations`,
989
+ async (req, res) => {
990
+ try {
991
+ const limit = req.query.limit
992
+ ? parseInt(req.query.limit as string, 10)
993
+ : undefined;
994
+ const offset = req.query.offset
995
+ ? parseInt(req.query.offset as string, 10)
996
+ : undefined;
997
+ const builds = await materializationController.listMaterializations(
998
+ req.params.projectName,
999
+ req.params.packageName,
1000
+ { limit, offset },
1001
+ );
1002
+ res.status(200).json(remapMaterializationResponse(builds));
1003
+ } catch (error) {
1004
+ const { json, status } = internalErrorToHttpError(error as Error);
1005
+ res.status(status).json(json);
1006
+ }
1007
+ },
1008
+ );
1009
+
1010
+ app.get(
1011
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1012
+ async (req, res) => {
1013
+ try {
1014
+ const build = await materializationController.getMaterialization(
1015
+ req.params.projectName,
1016
+ req.params.packageName,
1017
+ req.params.materializationId,
1018
+ );
1019
+ res.status(200).json(remapMaterializationResponse(build));
1020
+ } catch (error) {
1021
+ const { json, status } = internalErrorToHttpError(error as Error);
1022
+ res.status(status).json(json);
1023
+ }
1024
+ },
1025
+ );
1026
+
1027
+ app.post(
1028
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations/teardown`,
1029
+ async (req, res) => {
1030
+ try {
1031
+ const result = await materializationController.teardownPackage(
1032
+ req.params.projectName,
1033
+ req.params.packageName,
1034
+ req.body || {},
1035
+ );
1036
+ res.status(200).json(result);
1037
+ } catch (error) {
1038
+ const { json, status } = internalErrorToHttpError(error as Error);
1039
+ res.status(status).json(json);
1040
+ }
1041
+ },
1042
+ );
1043
+
1044
+ app.post(
1045
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1046
+ async (req, res) => {
1047
+ try {
1048
+ const action = req.query.action;
1049
+ if (action === "start") {
1050
+ const build =
1051
+ await materializationController.startMaterialization(
1052
+ req.params.projectName,
1053
+ req.params.packageName,
1054
+ req.params.materializationId,
1055
+ );
1056
+ res.status(202).json(remapMaterializationResponse(build));
1057
+ } else if (action === "stop") {
1058
+ const build =
1059
+ await materializationController.stopMaterialization(
1060
+ req.params.projectName,
1061
+ req.params.packageName,
1062
+ req.params.materializationId,
1063
+ );
1064
+ res.status(200).json(remapMaterializationResponse(build));
1065
+ } else {
1066
+ throw new BadRequestError(
1067
+ `Unsupported action '${String(action ?? "")}'. Expected 'start' or 'stop'.`,
1068
+ );
1069
+ }
1070
+ } catch (error) {
1071
+ const { json, status } = internalErrorToHttpError(error as Error);
1072
+ res.status(status).json(json);
1073
+ }
1074
+ },
1075
+ );
1076
+
1077
+ app.delete(
1078
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1079
+ async (req, res) => {
1080
+ try {
1081
+ await materializationController.deleteMaterialization(
1082
+ req.params.projectName,
1083
+ req.params.packageName,
1084
+ req.params.materializationId,
1085
+ );
1086
+ res.status(204).send();
1087
+ } catch (error) {
1088
+ const { json, status } = internalErrorToHttpError(error as Error);
1089
+ res.status(status).json(json);
1090
+ }
1091
+ },
1092
+ );
1093
+
1094
+ // ── manifest ────────────────────────────────────────────────────────────
1095
+ app.get(
1096
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
1097
+ async (req, res) => {
1098
+ try {
1099
+ const manifest = await manifestController.getManifest(
1100
+ req.params.projectName,
1101
+ req.params.packageName,
1102
+ );
1103
+ res.status(200).json(manifest);
1104
+ } catch (error) {
1105
+ logger.error("Get manifest error", { error });
1106
+ const { json, status } = internalErrorToHttpError(error as Error);
1107
+ res.status(status).json(json);
1108
+ }
1109
+ },
1110
+ );
1111
+
1112
+ app.post(
1113
+ `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
1114
+ async (req, res) => {
1115
+ try {
1116
+ const action = req.query.action;
1117
+ if (action === "reload") {
1118
+ const manifest = await manifestController.reloadManifest(
1119
+ req.params.projectName,
1120
+ req.params.packageName,
1121
+ );
1122
+ res.status(200).json(manifest);
1123
+ } else {
1124
+ throw new BadRequestError(
1125
+ `Unsupported action '${String(action ?? "")}'. Expected 'reload'.`,
1126
+ );
1127
+ }
1128
+ } catch (error) {
1129
+ logger.error("Manifest action error", { error });
1130
+ const { json, status } = internalErrorToHttpError(error as Error);
1131
+ res.status(status).json(json);
1132
+ }
1133
+ },
1134
+ );
1135
+
1136
+ logger.info(
1137
+ "Legacy /projects/* routes registered for backwards compatibility",
1138
+ );
1139
+ }