@malloy-publisher/server 0.0.382-dev → 0.2.1

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.
Files changed (175) hide show
  1. package/README.docker.md +145 -20
  2. package/README.md +65 -12
  3. package/dist/app/api-doc.html +5 -0
  4. package/dist/app/api-doc.yaml +3929 -534
  5. package/dist/app/assets/EnvironmentPage-CHq5BF4l.js +1 -0
  6. package/dist/app/assets/HomePage-CIp-pJCa.js +1 -0
  7. package/dist/app/assets/LightMode-mHHPjEeA.js +1 -0
  8. package/dist/app/assets/MainPage-CylxO5Yu.js +2 -0
  9. package/dist/app/assets/MaterializationsPage-a0vjqxib.js +1 -0
  10. package/dist/app/assets/ModelPage-Cz-MrSZQ.js +1 -0
  11. package/dist/app/assets/PackagePage-Cdg5D7AN.js +1 -0
  12. package/dist/app/assets/RouteError-B5SIyn6G.js +1 -0
  13. package/dist/app/assets/ThemeEditorPage-B62rRc-y.js +1 -0
  14. package/dist/app/assets/WorkbookPage-45cG1Gx6.js +1 -0
  15. package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-DyU47reh.es-DY9b3WjS.js} +1 -1
  16. package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
  17. package/dist/app/assets/index-5eLCcNmP.css +1 -0
  18. package/dist/app/assets/index-C5ClsnS7.js +1774 -0
  19. package/dist/app/assets/index-DF9tVqwN.js +1 -0
  20. package/dist/app/assets/index-DSoCUF-f.js +2725 -0
  21. package/dist/app/assets/index-DgVRz2wm.js +2 -0
  22. package/dist/app/assets/index-P-pF8nU5.js +632 -0
  23. package/dist/app/assets/index-w_0OQJgZ.js +23 -0
  24. package/dist/app/index.html +8 -4
  25. package/dist/cpufeatures-1yrn0vtw.node +0 -0
  26. package/dist/default-publisher.config.json +23 -0
  27. package/dist/{instrumentation.js → instrumentation.mjs} +10631 -10573
  28. package/dist/package_load_worker.mjs +15219 -0
  29. package/dist/runtime/publisher.js +331 -0
  30. package/dist/{server.js → server.mjs} +141276 -74705
  31. package/dist/sshcrypto-vd2k5hq9.node +0 -0
  32. package/package.json +41 -28
  33. package/publisher.config.example.bigquery.json +33 -0
  34. package/publisher.config.example.duckdb.json +23 -0
  35. package/.eslintrc.json +0 -44
  36. package/.prettierignore +0 -1
  37. package/.prettierrc +0 -4
  38. package/build.ts +0 -38
  39. package/dist/app/assets/HomePage-Dn3E4CuB.js +0 -1
  40. package/dist/app/assets/MainPage-BzB3yoqi.js +0 -2
  41. package/dist/app/assets/ModelPage-C9O_sAXT.js +0 -1
  42. package/dist/app/assets/PackagePage-DcxKEjBX.js +0 -1
  43. package/dist/app/assets/ProjectPage-BDj307rF.js +0 -1
  44. package/dist/app/assets/RouteError-DAShbVCG.js +0 -1
  45. package/dist/app/assets/WorkbookPage-Cs_XYEaB.js +0 -1
  46. package/dist/app/assets/index-15BOvhp0.js +0 -456
  47. package/dist/app/assets/index-Bb2jqquW.js +0 -676
  48. package/dist/app/assets/index-CMlGQMcl.css +0 -1
  49. package/dist/app/assets/index-D68X76-7.js +0 -1276
  50. package/dist/app/assets/index.umd-DGBekgSu.js +0 -1145
  51. package/dxt/malloy_bridge.py +0 -354
  52. package/dxt/manifest.json +0 -22
  53. package/eslint.config.mjs +0 -76
  54. package/k6-tests/README.md +0 -144
  55. package/k6-tests/bun.lock +0 -797
  56. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  57. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  58. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  59. package/k6-tests/load-test/load-test-crud.ts +0 -142
  60. package/k6-tests/load-test/load-test.ts +0 -784
  61. package/k6-tests/package.json +0 -27
  62. package/k6-tests/scripts/run-with-otel.sh +0 -50
  63. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  64. package/k6-tests/tsconfig.json +0 -29
  65. package/k6-tests/utils/client_factory.ts +0 -128
  66. package/k6-tests/utils/common.ts +0 -489
  67. package/k6-tests/utils/logger.ts +0 -62
  68. package/k6-tests/utils/types.d.ts +0 -35
  69. package/malloy_mcp.dxt +0 -0
  70. package/publisher.config.json +0 -33
  71. package/src/config.spec.ts +0 -850
  72. package/src/config.ts +0 -305
  73. package/src/constants.ts +0 -10
  74. package/src/controller/compile.controller.ts +0 -35
  75. package/src/controller/connection.controller.ts +0 -515
  76. package/src/controller/database.controller.ts +0 -21
  77. package/src/controller/manifest.controller.ts +0 -29
  78. package/src/controller/materialization.controller.ts +0 -125
  79. package/src/controller/model.controller.ts +0 -107
  80. package/src/controller/package.controller.ts +0 -182
  81. package/src/controller/query.controller.ts +0 -60
  82. package/src/controller/watch-mode.controller.ts +0 -98
  83. package/src/data_styles.ts +0 -71
  84. package/src/dto/connection.dto.spec.ts +0 -131
  85. package/src/dto/connection.dto.ts +0 -174
  86. package/src/dto/index.ts +0 -2
  87. package/src/dto/package.dto.spec.ts +0 -42
  88. package/src/dto/package.dto.ts +0 -12
  89. package/src/dto/validate.spec.ts +0 -76
  90. package/src/dto/validate.ts +0 -31
  91. package/src/errors.ts +0 -116
  92. package/src/health.ts +0 -189
  93. package/src/instrumentation.ts +0 -148
  94. package/src/logger.ts +0 -154
  95. package/src/mcp/error_messages.ts +0 -199
  96. package/src/mcp/handler_utils.ts +0 -239
  97. package/src/mcp/mcp_constants.ts +0 -22
  98. package/src/mcp/prompts/handlers.ts +0 -84
  99. package/src/mcp/prompts/index.ts +0 -11
  100. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  101. package/src/mcp/prompts/prompt_service.ts +0 -67
  102. package/src/mcp/prompts/utils.ts +0 -62
  103. package/src/mcp/resource_metadata.ts +0 -47
  104. package/src/mcp/resources/model_resource.ts +0 -153
  105. package/src/mcp/resources/notebook_resource.ts +0 -137
  106. package/src/mcp/resources/package_resource.ts +0 -370
  107. package/src/mcp/resources/project_resource.ts +0 -184
  108. package/src/mcp/resources/query_resource.ts +0 -117
  109. package/src/mcp/resources/source_resource.ts +0 -141
  110. package/src/mcp/resources/view_resource.ts +0 -136
  111. package/src/mcp/server.ts +0 -62
  112. package/src/mcp/tools/discovery_tools.ts +0 -258
  113. package/src/mcp/tools/execute_query_tool.ts +0 -265
  114. package/src/server.ts +0 -1195
  115. package/src/service/connection.spec.ts +0 -1331
  116. package/src/service/connection.ts +0 -1511
  117. package/src/service/connection_service.spec.ts +0 -641
  118. package/src/service/connection_service.ts +0 -181
  119. package/src/service/db_utils.spec.ts +0 -714
  120. package/src/service/db_utils.ts +0 -1227
  121. package/src/service/filter.spec.ts +0 -447
  122. package/src/service/filter.ts +0 -337
  123. package/src/service/filter_integration.spec.ts +0 -825
  124. package/src/service/gcs_s3_utils.ts +0 -427
  125. package/src/service/manifest_service.spec.ts +0 -201
  126. package/src/service/manifest_service.ts +0 -106
  127. package/src/service/materialization_service.spec.ts +0 -648
  128. package/src/service/materialization_service.ts +0 -929
  129. package/src/service/materialized_table_gc.spec.ts +0 -383
  130. package/src/service/materialized_table_gc.ts +0 -279
  131. package/src/service/model.spec.ts +0 -257
  132. package/src/service/model.ts +0 -1069
  133. package/src/service/package.spec.ts +0 -236
  134. package/src/service/package.ts +0 -463
  135. package/src/service/project.ts +0 -681
  136. package/src/service/project_compile.spec.ts +0 -197
  137. package/src/service/project_store.spec.ts +0 -859
  138. package/src/service/project_store.ts +0 -1531
  139. package/src/service/quoting.ts +0 -41
  140. package/src/service/resolve_project.ts +0 -13
  141. package/src/storage/BaseRepository.ts +0 -31
  142. package/src/storage/DatabaseInterface.ts +0 -180
  143. package/src/storage/StorageManager.mock.ts +0 -59
  144. package/src/storage/StorageManager.ts +0 -210
  145. package/src/storage/duckdb/ConnectionRepository.ts +0 -141
  146. package/src/storage/duckdb/DuckDBConnection.ts +0 -177
  147. package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
  148. package/src/storage/duckdb/DuckDBRepository.ts +0 -224
  149. package/src/storage/duckdb/ManifestRepository.ts +0 -119
  150. package/src/storage/duckdb/MaterializationRepository.ts +0 -249
  151. package/src/storage/duckdb/PackageRepository.ts +0 -135
  152. package/src/storage/duckdb/ProjectRepository.ts +0 -148
  153. package/src/storage/duckdb/manifest_store.spec.ts +0 -133
  154. package/src/storage/duckdb/schema.ts +0 -147
  155. package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -146
  156. package/src/utils.ts +0 -13
  157. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  158. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  159. package/tests/fixtures/persist-test/publisher.json +0 -5
  160. package/tests/fixtures/publisher.config.json +0 -15
  161. package/tests/harness/e2e.ts +0 -96
  162. package/tests/harness/mcp_test_setup.ts +0 -344
  163. package/tests/harness/mocks.ts +0 -37
  164. package/tests/harness/rest_e2e.ts +0 -68
  165. package/tests/harness/uris.ts +0 -31
  166. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -470
  167. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -434
  168. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -663
  169. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -122
  170. package/tests/integration/mcp/setup.spec.ts +0 -5
  171. package/tests/unit/duckdb/attached_databases.test.ts +0 -1132
  172. package/tests/unit/ducklake/ducklake.test.ts +0 -950
  173. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  174. package/tests/unit/mcp/prompt_happy.test.ts +0 -51
  175. package/tsconfig.json +0 -32
@@ -1,41 +0,0 @@
1
- /**
2
- * Minimal identifier-quoting surface. Every `Dialect` in `@malloydata/malloy`
3
- * implements this; we accept the duck type so tests can inject a fake without
4
- * instantiating a full dialect.
5
- */
6
- export interface Quoter {
7
- quoteTablePath(seg: string): string;
8
- }
9
-
10
- /**
11
- * Quote a potentially schema-qualified table path (e.g. "schema.table")
12
- * by quoting each segment individually with the dialect's quoteTablePath.
13
- */
14
- export function quoteTablePath(path: string, dialect: Quoter): string {
15
- return path
16
- .split(".")
17
- .map((seg) => dialect.quoteTablePath(seg))
18
- .join(".");
19
- }
20
-
21
- /**
22
- * Split a possibly schema-qualified table name into its schema prefix
23
- * (including the trailing dot) and the bare table name.
24
- *
25
- * Examples:
26
- * "my_schema.my_table" -> { schemaPrefix: "my_schema.", bareName: "my_table" }
27
- * "my_table" -> { schemaPrefix: "", bareName: "my_table" }
28
- */
29
- export function splitTablePath(tableName: string): {
30
- schemaPrefix: string;
31
- bareName: string;
32
- } {
33
- const lastDot = tableName.lastIndexOf(".");
34
- if (lastDot >= 0) {
35
- return {
36
- schemaPrefix: tableName.substring(0, lastDot + 1),
37
- bareName: tableName.substring(lastDot + 1),
38
- };
39
- }
40
- return { schemaPrefix: "", bareName: tableName };
41
- }
@@ -1,13 +0,0 @@
1
- import { ProjectNotFoundError } from "../errors";
2
- import { ResourceRepository } from "../storage/DatabaseInterface";
3
-
4
- export async function resolveProjectId(
5
- repository: ResourceRepository,
6
- projectName: string,
7
- ): Promise<string> {
8
- const dbProject = await repository.getProjectByName(projectName);
9
- if (!dbProject) {
10
- throw new ProjectNotFoundError(`Project '${projectName}' not found`);
11
- }
12
- return dbProject.id;
13
- }
@@ -1,31 +0,0 @@
1
- import { DatabaseConnection } from "./DatabaseInterface";
2
-
3
- export abstract class BaseRepository {
4
- constructor(protected db: DatabaseConnection) {}
5
-
6
- protected generateId(): string {
7
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
8
- }
9
-
10
- protected now(): Date {
11
- return new Date();
12
- }
13
-
14
- protected async executeQuery<T>(
15
- _query: string,
16
- _params?: unknown[],
17
- ): Promise<T[]> {
18
- // This will be implemented by specific database implementations
19
- throw new Error(
20
- "executeQuery must be implemented by database-specific class",
21
- );
22
- }
23
-
24
- protected async executeOne<T>(
25
- query: string,
26
- params?: unknown[],
27
- ): Promise<T | null> {
28
- const results = await this.executeQuery<T>(query, params);
29
- return results.length > 0 ? results[0] : null;
30
- }
31
- }
@@ -1,180 +0,0 @@
1
- /**
2
- * Generic database interface for storage abstraction
3
- * This allows switching between DuckDB, PostgreSQL, MySQL, etc.
4
- */
5
- export interface DatabaseConnection {
6
- initialize(): Promise<void>;
7
- close(): Promise<void>;
8
- isInitialized(): Promise<boolean>;
9
- }
10
-
11
- export interface ResourceRepository {
12
- // Projects
13
- listProjects(): Promise<Project[]>;
14
- getProjectById(id: string): Promise<Project | null>;
15
- getProjectByName(name: string): Promise<Project | null>;
16
- createProject(
17
- project: Omit<Project, "id" | "createdAt" | "updatedAt">,
18
- ): Promise<Project>;
19
- updateProject(id: string, updates: Partial<Project>): Promise<Project>;
20
- deleteProject(id: string): Promise<void>;
21
-
22
- // Packages
23
- listPackages(projectId: string): Promise<Package[]>;
24
- getPackageById(id: string): Promise<Package | null>;
25
- getPackageByName(projectId: string, name: string): Promise<Package | null>;
26
- createPackage(
27
- pkg: Omit<Package, "id" | "createdAt" | "updatedAt">,
28
- ): Promise<Package>;
29
- updatePackage(id: string, updates: Partial<Package>): Promise<Package>;
30
- deletePackage(id: string): Promise<void>;
31
-
32
- // Connections
33
- listConnections(projectId: string): Promise<Connection[]>;
34
- getConnectionById(id: string): Promise<Connection | null>;
35
- getConnectionByName(
36
- projectId: string,
37
- name: string,
38
- ): Promise<Connection | null>;
39
- createConnection(
40
- connection: Omit<Connection, "id" | "createdAt" | "updatedAt">,
41
- ): Promise<Connection>;
42
- updateConnection(
43
- id: string,
44
- updates: Partial<Connection>,
45
- ): Promise<Connection>;
46
- deleteConnection(id: string): Promise<void>;
47
-
48
- // Materializations
49
- listMaterializations(
50
- projectId: string,
51
- packageName: string,
52
- options?: { limit?: number; offset?: number },
53
- ): Promise<Materialization[]>;
54
- getMaterializationById(id: string): Promise<Materialization | null>;
55
- getActiveMaterialization(
56
- projectId: string,
57
- packageName: string,
58
- ): Promise<Materialization | null>;
59
- createMaterialization(
60
- projectId: string,
61
- packageName: string,
62
- status?: MaterializationStatus,
63
- metadata?: Record<string, unknown> | null,
64
- ): Promise<Materialization>;
65
- updateMaterialization(
66
- id: string,
67
- updates: {
68
- status?: MaterializationStatus;
69
- startedAt?: Date;
70
- completedAt?: Date;
71
- error?: string | null;
72
- metadata?: Record<string, unknown> | null;
73
- },
74
- ): Promise<Materialization>;
75
- deleteMaterialization(id: string): Promise<void>;
76
- // Build Manifests
77
- listManifestEntries(
78
- projectId: string,
79
- packageName: string,
80
- ): Promise<ManifestEntry[]>;
81
- upsertManifestEntry(
82
- entry: Omit<ManifestEntry, "id" | "createdAt" | "updatedAt">,
83
- ): Promise<ManifestEntry>;
84
- deleteManifestEntry(id: string): Promise<void>;
85
- }
86
-
87
- export interface Project {
88
- id: string;
89
- name: string;
90
- path: string;
91
- description?: string;
92
- createdAt: Date;
93
- updatedAt: Date;
94
- metadata?: Record<string, unknown>;
95
- }
96
-
97
- export interface Package {
98
- id: string;
99
- projectId: string;
100
- name: string;
101
- description?: string;
102
- manifestPath: string;
103
- createdAt: Date;
104
- updatedAt: Date;
105
- metadata?: Record<string, unknown>;
106
- }
107
-
108
- export interface Connection {
109
- id: string;
110
- projectId: string;
111
- name: string;
112
- type: "bigquery" | "postgres" | "duckdb" | "mysql" | "snowflake" | "trino";
113
- config: Record<string, unknown>;
114
- createdAt: Date;
115
- updatedAt: Date;
116
- }
117
-
118
- export type MaterializationStatus =
119
- | "PENDING"
120
- | "RUNNING"
121
- | "SUCCESS"
122
- | "FAILED"
123
- | "CANCELLED";
124
-
125
- export interface Materialization {
126
- id: string;
127
- projectId: string;
128
- packageName: string;
129
- status: MaterializationStatus;
130
- startedAt: Date | null;
131
- completedAt: Date | null;
132
- error: string | null;
133
- metadata: Record<string, unknown> | null;
134
- createdAt: Date;
135
- updatedAt: Date;
136
- }
137
-
138
- export interface ManifestEntry {
139
- id: string;
140
- projectId: string;
141
- packageName: string;
142
- buildId: string;
143
- tableName: string;
144
- sourceName: string;
145
- connectionName: string;
146
- createdAt: Date;
147
- updatedAt: Date;
148
- }
149
-
150
- // ==================== MANIFEST STORE ====================
151
-
152
- export interface BuildManifestEntry {
153
- tableName: string;
154
- }
155
-
156
- export interface BuildManifest {
157
- entries: Record<string, BuildManifestEntry>;
158
- strict?: boolean;
159
- }
160
-
161
- /**
162
- * Abstraction for manifest storage. Standalone mode uses DuckDB;
163
- * orchestrated mode swaps in a DuckLakeManifestStore.
164
- */
165
- export interface ManifestStore {
166
- getManifest(projectId: string, packageName: string): Promise<BuildManifest>;
167
- writeEntry(
168
- projectId: string,
169
- packageName: string,
170
- buildId: string,
171
- tableName: string,
172
- sourceName: string,
173
- connectionName: string,
174
- ): Promise<void>;
175
- deleteEntry(id: string): Promise<void>;
176
- listEntries(
177
- projectId: string,
178
- packageName: string,
179
- ): Promise<ManifestEntry[]>;
180
- }
@@ -1,59 +0,0 @@
1
- type MockData = Record<string, unknown>;
2
-
3
- export class StorageManager {
4
- async initialize(): Promise<void> {
5
- return;
6
- }
7
-
8
- getRepository() {
9
- return {
10
- listProjects: async (): Promise<unknown[]> => [],
11
- createProject: async (data: MockData): Promise<MockData> => ({
12
- id: "test-id",
13
- ...data,
14
- }),
15
- updateProject: async (
16
- id: string,
17
- data: MockData,
18
- ): Promise<MockData> => ({
19
- id,
20
- ...data,
21
- }),
22
- getPackages: async (): Promise<unknown[]> => [],
23
- createPackage: async (data: MockData): Promise<MockData> => ({
24
- id: "test-id",
25
- ...data,
26
- }),
27
- updatePackage: async (
28
- id: string,
29
- data: MockData,
30
- ): Promise<MockData> => ({
31
- id,
32
- ...data,
33
- }),
34
- deletePackage: async (): Promise<void> => {},
35
- getConnections: async (): Promise<unknown[]> => [],
36
- createConnection: async (data: MockData): Promise<MockData> => ({
37
- id: "test-id",
38
- ...data,
39
- }),
40
- updateConnection: async (
41
- id: string,
42
- data: MockData,
43
- ): Promise<MockData> => ({
44
- id,
45
- ...data,
46
- }),
47
- deleteConnection: async (): Promise<void> => {},
48
- };
49
- }
50
-
51
- getManifestStore() {
52
- return {
53
- getManifest: async () => ({ entries: {}, strict: false }),
54
- writeEntry: async () => {},
55
- deleteEntry: async () => {},
56
- listEntries: async () => [],
57
- };
58
- }
59
- }
@@ -1,210 +0,0 @@
1
- import { logger } from "../logger";
2
- import {
3
- DatabaseConnection,
4
- ManifestStore,
5
- ResourceRepository,
6
- } from "./DatabaseInterface";
7
- import { DuckDBConnection } from "./duckdb/DuckDBConnection";
8
- import { DuckDBManifestStore } from "./duckdb/DuckDBManifestStore";
9
- import { DuckDBRepository } from "./duckdb/DuckDBRepository";
10
- import { initializeSchema } from "./duckdb/schema";
11
- import { DuckLakeManifestStore } from "./ducklake/DuckLakeManifestStore";
12
-
13
- export type StorageType = "duckdb" | "postgres" | "mysql";
14
-
15
- export interface DuckLakeManifestConfig {
16
- catalogUrl: string;
17
- dataPath: string;
18
- }
19
-
20
- export interface StorageConfig {
21
- type: StorageType;
22
- duckdb?: {
23
- path?: string;
24
- };
25
- postgres?: {
26
- host: string;
27
- port: number;
28
- database: string;
29
- user: string;
30
- password: string;
31
- };
32
- mysql?: {
33
- host: string;
34
- port: number;
35
- database: string;
36
- user: string;
37
- password: string;
38
- };
39
- }
40
-
41
- function escapeSQL(value: string): string {
42
- return value.replace(/'/g, "''");
43
- }
44
-
45
- /**
46
- * Manages the storage backend (DuckDB, Postgres, etc.) and per-project
47
- * manifest stores. Projects without `materializationStorage` config use
48
- * the default DuckDB manifest store. Projects with the config get a
49
- * DuckLake-backed store attached lazily on first access.
50
- */
51
- export class StorageManager {
52
- private connection: DatabaseConnection | null = null;
53
- private duckDbConnection: DuckDBConnection | null = null;
54
- private repository: ResourceRepository | null = null;
55
- private defaultManifestStore: ManifestStore | null = null;
56
-
57
- /** Per-project DuckLake manifest stores, keyed by projectId. */
58
- private projectManifestStores = new Map<string, ManifestStore>();
59
-
60
- /** Tracks which catalogs have been attached to avoid duplicate ATTACHes. */
61
- private attachedCatalogs = new Set<string>();
62
-
63
- private config: StorageConfig;
64
-
65
- constructor(config: StorageConfig) {
66
- this.config = config;
67
- }
68
-
69
- async initialize(reinit: boolean = false): Promise<void> {
70
- if (reinit) {
71
- logger.info(
72
- "Reinitialization mode: Database will be dropped and recreated",
73
- );
74
- } else {
75
- logger.info("Normal mode: Loading from existing database");
76
- }
77
-
78
- switch (this.config.type) {
79
- case "duckdb":
80
- await this.initializeDuckDB(reinit);
81
- break;
82
- case "postgres":
83
- throw new Error("PostgreSQL storage not yet implemented");
84
- case "mysql":
85
- throw new Error("MySQL storage not yet implemented");
86
- default:
87
- throw new Error(`Unsupported storage type: ${this.config.type}`);
88
- }
89
- }
90
-
91
- private async initializeDuckDB(reinit: boolean): Promise<void> {
92
- const dbPath = this.config.duckdb?.path;
93
-
94
- const connection = new DuckDBConnection(dbPath);
95
-
96
- await connection.initialize();
97
-
98
- await initializeSchema(connection, reinit);
99
-
100
- this.connection = connection;
101
- this.duckDbConnection = connection;
102
- this.repository = new DuckDBRepository(connection);
103
- this.defaultManifestStore = new DuckDBManifestStore(this.repository);
104
- }
105
-
106
- /**
107
- * Lazily initializes a DuckLake manifest store for a project.
108
- * The DuckLake catalog is attached to the shared DuckDB connection
109
- * and persists for the lifetime of the process.
110
- */
111
- async initializeDuckLakeForProject(
112
- projectId: string,
113
- config: DuckLakeManifestConfig,
114
- ): Promise<void> {
115
- if (!this.duckDbConnection) {
116
- throw new Error("Storage not initialized. Call initialize() first.");
117
- }
118
-
119
- const catalogName = `manifest_lake_${projectId.replace(/[^a-zA-Z0-9_]/g, "_")}`;
120
-
121
- if (!this.attachedCatalogs.has(catalogName)) {
122
- await this.attachDuckLakeCatalog(config, catalogName);
123
- }
124
-
125
- const store = new DuckLakeManifestStore(
126
- this.duckDbConnection,
127
- catalogName,
128
- );
129
- await store.bootstrapSchema();
130
-
131
- this.projectManifestStores.set(projectId, store);
132
- logger.info("DuckLake manifest store initialized for project", {
133
- projectId,
134
- catalogName,
135
- });
136
- }
137
-
138
- private async attachDuckLakeCatalog(
139
- config: DuckLakeManifestConfig,
140
- catalogName: string,
141
- ): Promise<void> {
142
- const connection = this.duckDbConnection!;
143
-
144
- await connection.run("INSTALL ducklake; LOAD ducklake;");
145
-
146
- const isPostgres = config.catalogUrl.startsWith("postgres:");
147
- if (isPostgres) {
148
- await connection.run("INSTALL postgres; LOAD postgres;");
149
- }
150
-
151
- const escapedCatalogUrl = escapeSQL(config.catalogUrl);
152
- const escapedDataPath = escapeSQL(config.dataPath);
153
- const isCloudStorage =
154
- config.dataPath.startsWith("gs://") ||
155
- config.dataPath.startsWith("s3://");
156
-
157
- let attachCmd = `ATTACH 'ducklake:${escapedCatalogUrl}' AS ${catalogName}`;
158
- const attachOpts: string[] = [`DATA_PATH '${escapedDataPath}'`];
159
- if (isCloudStorage) {
160
- attachOpts.push("OVERRIDE_DATA_PATH true");
161
- }
162
- attachCmd += ` (${attachOpts.join(", ")});`;
163
-
164
- logger.info(`Attaching DuckLake manifest catalog: ${attachCmd}`);
165
- await connection.run(attachCmd);
166
-
167
- this.attachedCatalogs.add(catalogName);
168
- }
169
-
170
- getRepository(): ResourceRepository {
171
- if (!this.repository) {
172
- throw new Error("Storage not initialized. Call initialize() first.");
173
- }
174
- return this.repository;
175
- }
176
-
177
- /**
178
- * Returns the manifest store for a project. If the project has a
179
- * DuckLake store configured, returns that; otherwise returns the
180
- * default DuckDB-backed store.
181
- */
182
- getManifestStore(projectId?: string): ManifestStore {
183
- if (projectId) {
184
- const projectStore = this.projectManifestStores.get(projectId);
185
- if (projectStore) {
186
- return projectStore;
187
- }
188
- }
189
- if (!this.defaultManifestStore) {
190
- throw new Error("Storage not initialized. Call initialize() first.");
191
- }
192
- return this.defaultManifestStore;
193
- }
194
-
195
- async close(): Promise<void> {
196
- if (this.connection) {
197
- await this.connection.close();
198
- this.connection = null;
199
- this.duckDbConnection = null;
200
- this.repository = null;
201
- this.defaultManifestStore = null;
202
- this.projectManifestStores.clear();
203
- this.attachedCatalogs.clear();
204
- }
205
- }
206
-
207
- isInitialized(): boolean {
208
- return this.connection !== null && this.repository !== null;
209
- }
210
- }
@@ -1,141 +0,0 @@
1
- import { Connection } from "../DatabaseInterface";
2
- import { DuckDBConnection } from "./DuckDBConnection";
3
-
4
- export class ConnectionRepository {
5
- constructor(private db: DuckDBConnection) {}
6
-
7
- private generateId(): string {
8
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
9
- }
10
-
11
- private now(): Date {
12
- return new Date();
13
- }
14
-
15
- async listConnections(projectId: string): Promise<Connection[]> {
16
- try {
17
- const rows = await this.db.all<Record<string, unknown>>(
18
- "SELECT * FROM connections WHERE project_id = ? ORDER BY name",
19
- [projectId],
20
- );
21
- return rows.map(this.mapToConnection);
22
- } catch (err: unknown) {
23
- const error = err as Error;
24
- console.error("Failed to get connections:", error.message);
25
- throw error;
26
- }
27
- }
28
-
29
- async getConnectionById(id: string): Promise<Connection | null> {
30
- const row = await this.db.get<Record<string, unknown>>(
31
- "SELECT * FROM connections WHERE id = ?",
32
- [id],
33
- );
34
- return row ? this.mapToConnection(row) : null;
35
- }
36
-
37
- async getConnectionByName(
38
- projectId: string,
39
- name: string,
40
- ): Promise<Connection | null> {
41
- const row = await this.db.get<Record<string, unknown>>(
42
- "SELECT * FROM connections WHERE project_id = ? AND name = ?",
43
- [projectId, name],
44
- );
45
- return row ? this.mapToConnection(row) : null;
46
- }
47
-
48
- async createConnection(
49
- connection: Omit<Connection, "id" | "createdAt" | "updatedAt">,
50
- ): Promise<Connection> {
51
- const id = this.generateId();
52
- const now = this.now();
53
-
54
- try {
55
- const configJson = JSON.stringify(connection.config);
56
-
57
- await this.db.run(
58
- `INSERT INTO connections (id, project_id, name, type, config, created_at, updated_at)
59
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
60
- [
61
- id,
62
- connection.projectId,
63
- connection.name,
64
- connection.type,
65
- configJson,
66
- now.toISOString(),
67
- now.toISOString(),
68
- ],
69
- );
70
-
71
- return {
72
- id,
73
- ...connection,
74
- createdAt: now,
75
- updatedAt: now,
76
- };
77
- } catch (err: unknown) {
78
- const error = err as Error;
79
- console.error("Failed to create connection:", error.message);
80
- throw error;
81
- }
82
- }
83
-
84
- async updateConnection(
85
- id: string,
86
- updates: Partial<Connection>,
87
- ): Promise<Connection> {
88
- const existing = await this.getConnectionById(id);
89
- if (!existing) {
90
- throw new Error(`Connection with id ${id} not found`);
91
- }
92
-
93
- const now = this.now();
94
- const setClauses: string[] = [];
95
- const params: unknown[] = [];
96
-
97
- if (updates.name !== undefined) {
98
- setClauses.push(`name = ?`);
99
- params.push(updates.name);
100
- }
101
- if (updates.type !== undefined) {
102
- setClauses.push(`type = ?`);
103
- params.push(updates.type);
104
- }
105
- if (updates.config !== undefined) {
106
- setClauses.push(`config = ?`);
107
- params.push(JSON.stringify(updates.config));
108
- }
109
-
110
- setClauses.push(`updated_at = ?`);
111
- params.push(now.toISOString());
112
- params.push(id);
113
-
114
- await this.db.run(
115
- `UPDATE connections SET ${setClauses.join(", ")} WHERE id = ?`,
116
- params,
117
- );
118
-
119
- return this.getConnectionById(id) as Promise<Connection>;
120
- }
121
-
122
- async deleteConnection(id: string): Promise<void> {
123
- await this.db.run("DELETE FROM connections WHERE id = ?", [id]);
124
- }
125
-
126
- async deleteConnectionsByProjectId(id: string): Promise<void> {
127
- await this.db.run("DELETE FROM connections WHERE project_id = ?", [id]);
128
- }
129
-
130
- private mapToConnection(row: Record<string, unknown>): Connection {
131
- return {
132
- id: row.id as string,
133
- projectId: row.project_id as string,
134
- name: row.name as string,
135
- type: row.type as Connection["type"],
136
- config: JSON.parse(row.config as string),
137
- createdAt: new Date(row.created_at as string),
138
- updatedAt: new Date(row.updated_at as string),
139
- };
140
- }
141
- }