@malloy-publisher/server 0.0.382-dev → 0.2.0

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 +3903 -583
  5. package/dist/app/assets/EnvironmentPage-DiUgLuCH.js +1 -0
  6. package/dist/app/assets/HomePage-MhhjhnFj.js +1 -0
  7. package/dist/app/assets/LightMode-BEyvNUzT.js +1 -0
  8. package/dist/app/assets/MainPage-D4Cfee9o.js +2 -0
  9. package/dist/app/assets/MaterializationsPage-nGgGqwSN.js +1 -0
  10. package/dist/app/assets/ModelPage-DLAcEw-F.js +1 -0
  11. package/dist/app/assets/PackagePage-Df0ikH7w.js +1 -0
  12. package/dist/app/assets/RouteError-BFQwtroH.js +1 -0
  13. package/dist/app/assets/ThemeEditorPage-OoskoQYN.js +1 -0
  14. package/dist/app/assets/WorkbookPage-CpFgvUDC.js +1 -0
  15. package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-lb28vQkR.es-BoVSOc75.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-6ypuFH6g.js +1774 -0
  19. package/dist/app/assets/index-B8pnU_V_.js +615 -0
  20. package/dist/app/assets/index-BImcHmVP.js +1 -0
  21. package/dist/app/assets/index-DSoCUF-f.js +2725 -0
  22. package/dist/app/assets/index-DfqzSItA.js +2 -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} +115548 -49590
  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,249 +0,0 @@
1
- import { Materialization, MaterializationStatus } from "../DatabaseInterface";
2
- import { DuckDBConnection } from "./DuckDBConnection";
3
-
4
- const TERMINAL_STATUSES: ReadonlySet<MaterializationStatus> = new Set([
5
- "SUCCESS",
6
- "FAILED",
7
- "CANCELLED",
8
- ]);
9
-
10
- function activeKeyFor(projectId: string, packageName: string): string {
11
- return `${projectId}|${packageName}`;
12
- }
13
-
14
- /**
15
- * Thrown when an atomic insert loses a race on (project, package) active
16
- * materialization. Surfaced separately from a generic DB error so the service
17
- * layer can translate to `MaterializationConflictError`.
18
- */
19
- export class DuplicateActiveMaterializationError extends Error {
20
- constructor(projectId: string, packageName: string) {
21
- super(
22
- `Active materialization already exists for (${projectId}, ${packageName})`,
23
- );
24
- }
25
- }
26
-
27
- /**
28
- * DuckDB-backed repository for package materializations.
29
- *
30
- * A Materialization tracks a single build run for a (project, package) pair
31
- * through its lifecycle: PENDING -> RUNNING -> SUCCESS | FAILED | CANCELLED.
32
- */
33
- export class MaterializationRepository {
34
- constructor(private db: DuckDBConnection) {}
35
-
36
- private generateId(): string {
37
- return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
38
- }
39
-
40
- private now(): Date {
41
- return new Date();
42
- }
43
-
44
- async list(
45
- projectId: string,
46
- packageName: string,
47
- options?: { limit?: number; offset?: number },
48
- ): Promise<Materialization[]> {
49
- let sql =
50
- "SELECT * FROM materializations WHERE project_id = ? AND package_name = ? ORDER BY created_at DESC";
51
- const params: unknown[] = [projectId, packageName];
52
- if (options?.limit !== undefined) {
53
- sql += " LIMIT ?";
54
- params.push(options.limit);
55
- }
56
- if (options?.offset !== undefined) {
57
- sql += " OFFSET ?";
58
- params.push(options.offset);
59
- }
60
- const rows = await this.db.all<Record<string, unknown>>(sql, params);
61
- return rows.map(this.mapRow);
62
- }
63
-
64
- async getById(id: string): Promise<Materialization | null> {
65
- const row = await this.db.get<Record<string, unknown>>(
66
- "SELECT * FROM materializations WHERE id = ?",
67
- [id],
68
- );
69
- return row ? this.mapRow(row) : null;
70
- }
71
-
72
- async getActive(
73
- projectId: string,
74
- packageName: string,
75
- ): Promise<Materialization | null> {
76
- const row = await this.db.get<Record<string, unknown>>(
77
- "SELECT * FROM materializations WHERE project_id = ? AND package_name = ? AND status IN ('PENDING', 'RUNNING')",
78
- [projectId, packageName],
79
- );
80
- return row ? this.mapRow(row) : null;
81
- }
82
-
83
- async create(
84
- projectId: string,
85
- packageName: string,
86
- status: MaterializationStatus = "PENDING",
87
- metadata: Record<string, unknown> | null = null,
88
- ): Promise<Materialization> {
89
- const id = this.generateId();
90
- const now = this.now();
91
- const iso = now.toISOString();
92
- // Set active_key iff the row is in a non-terminal state. The unique
93
- // index on active_key makes the race-free conditional insert: a second
94
- // concurrent create on the same (project, package) fails here rather
95
- // than in a check-then-write window.
96
- const activeKey = TERMINAL_STATUSES.has(status)
97
- ? null
98
- : activeKeyFor(projectId, packageName);
99
- const metadataJson = metadata ? JSON.stringify(metadata) : null;
100
-
101
- try {
102
- const rows = await this.db.all<Record<string, unknown>>(
103
- `INSERT INTO materializations (id, project_id, package_name, status, active_key, metadata, created_at, updated_at)
104
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
105
- RETURNING *`,
106
- [
107
- id,
108
- projectId,
109
- packageName,
110
- status,
111
- activeKey,
112
- metadataJson,
113
- iso,
114
- iso,
115
- ],
116
- );
117
- return this.mapRow(rows[0]);
118
- } catch (err) {
119
- if (isUniqueViolation(err, "idx_materializations_active_key")) {
120
- throw new DuplicateActiveMaterializationError(
121
- projectId,
122
- packageName,
123
- );
124
- }
125
- throw err;
126
- }
127
- }
128
-
129
- async update(
130
- id: string,
131
- updates: {
132
- status?: MaterializationStatus;
133
- startedAt?: Date;
134
- completedAt?: Date;
135
- error?: string | null;
136
- metadata?: Record<string, unknown> | null;
137
- },
138
- ): Promise<Materialization> {
139
- const now = this.now();
140
- const setClauses: string[] = [];
141
- const params: unknown[] = [];
142
-
143
- if (updates.status !== undefined) {
144
- setClauses.push(`status = ?`);
145
- params.push(updates.status);
146
- // Clear active_key on any transition to a terminal state; set it on
147
- // any transition to a non-terminal state. The unique index
148
- // guarantees we can never end up with two active rows for the same
149
- // (project, package).
150
- if (TERMINAL_STATUSES.has(updates.status)) {
151
- setClauses.push(`active_key = NULL`);
152
- } else {
153
- setClauses.push(`active_key = project_id || '|' || package_name`);
154
- }
155
- }
156
- if (updates.startedAt !== undefined) {
157
- setClauses.push(`started_at = ?`);
158
- params.push(updates.startedAt.toISOString());
159
- }
160
- if (updates.completedAt !== undefined) {
161
- setClauses.push(`completed_at = ?`);
162
- params.push(updates.completedAt.toISOString());
163
- }
164
- if (updates.error !== undefined) {
165
- setClauses.push(`error = ?`);
166
- params.push(updates.error);
167
- }
168
- if (updates.metadata !== undefined) {
169
- setClauses.push(`metadata = ?`);
170
- params.push(
171
- updates.metadata ? JSON.stringify(updates.metadata) : null,
172
- );
173
- }
174
-
175
- setClauses.push(`updated_at = ?`);
176
- params.push(now.toISOString());
177
- params.push(id);
178
-
179
- await this.db.run(
180
- `UPDATE materializations SET ${setClauses.join(", ")} WHERE id = ?`,
181
- params,
182
- );
183
-
184
- const updated = await this.getById(id);
185
- if (!updated) {
186
- throw new Error(`Materialization ${id} not found after update`);
187
- }
188
- return updated;
189
- }
190
-
191
- async deleteByProjectId(projectId: string): Promise<void> {
192
- await this.db.run("DELETE FROM materializations WHERE project_id = ?", [
193
- projectId,
194
- ]);
195
- }
196
-
197
- async deleteById(id: string): Promise<void> {
198
- await this.db.run("DELETE FROM materializations WHERE id = ?", [id]);
199
- }
200
-
201
- async deleteByPackage(
202
- projectId: string,
203
- packageName: string,
204
- ): Promise<void> {
205
- await this.db.run(
206
- "DELETE FROM materializations WHERE project_id = ? AND package_name = ?",
207
- [projectId, packageName],
208
- );
209
- }
210
-
211
- private mapRow(row: Record<string, unknown>): Materialization {
212
- let metadata: Record<string, unknown> | null = null;
213
- if (row.metadata) {
214
- try {
215
- metadata = JSON.parse(row.metadata as string);
216
- } catch {
217
- metadata = null;
218
- }
219
- }
220
-
221
- return {
222
- id: row.id as string,
223
- projectId: row.project_id as string,
224
- packageName: row.package_name as string,
225
- status: row.status as MaterializationStatus,
226
- startedAt: row.started_at ? new Date(row.started_at as string) : null,
227
- completedAt: row.completed_at
228
- ? new Date(row.completed_at as string)
229
- : null,
230
- error: row.error != null ? (row.error as string) : null,
231
- metadata,
232
- createdAt: new Date(row.created_at as string),
233
- updatedAt: new Date(row.updated_at as string),
234
- };
235
- }
236
- }
237
-
238
- /**
239
- * DuckDB surfaces unique-constraint violations as plain Errors whose message
240
- * mentions the violated index. We match on the index name rather than a
241
- * generic substring so we don't misclassify unrelated constraint errors.
242
- */
243
- function isUniqueViolation(err: unknown, indexName: string): boolean {
244
- if (!(err instanceof Error)) return false;
245
- const msg = err.message;
246
- return (
247
- msg.includes(indexName) || /duplicate key|unique constraint/i.test(msg)
248
- );
249
- }
@@ -1,135 +0,0 @@
1
- import { Package } from "../DatabaseInterface";
2
- import { DuckDBConnection } from "./DuckDBConnection";
3
-
4
- export class PackageRepository {
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 listPackages(projectId: string): Promise<Package[]> {
16
- const rows = await this.db.all<Record<string, unknown>>(
17
- "SELECT * FROM packages WHERE project_id = ? ORDER BY name",
18
- [projectId],
19
- );
20
- return rows.map(this.mapToPackage);
21
- }
22
-
23
- async getPackageById(id: string): Promise<Package | null> {
24
- const row = await this.db.get<Record<string, unknown>>(
25
- "SELECT * FROM packages WHERE id = ?",
26
- [id],
27
- );
28
- return row ? this.mapToPackage(row) : null;
29
- }
30
-
31
- async getPackageByName(
32
- projectId: string,
33
- name: string,
34
- ): Promise<Package | null> {
35
- const row = await this.db.get<Record<string, unknown>>(
36
- "SELECT * FROM packages WHERE project_id = ? AND name = ?",
37
- [projectId, name],
38
- );
39
- return row ? this.mapToPackage(row) : null;
40
- }
41
-
42
- async createPackage(
43
- pkg: Omit<Package, "id" | "createdAt" | "updatedAt">,
44
- ): Promise<Package> {
45
- const id = this.generateId();
46
- const now = this.now();
47
-
48
- await this.db.run(
49
- `INSERT INTO packages (id, project_id, name, description, manifest_path, metadata, created_at, updated_at)
50
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
51
- [
52
- id,
53
- pkg.projectId,
54
- pkg.name,
55
- pkg.description || null,
56
- pkg.manifestPath,
57
- pkg.metadata ? JSON.stringify(pkg.metadata) : null,
58
- now.toISOString(),
59
- now.toISOString(),
60
- ],
61
- );
62
-
63
- return {
64
- id,
65
- ...pkg,
66
- createdAt: now,
67
- updatedAt: now,
68
- };
69
- }
70
-
71
- async updatePackage(
72
- id: string,
73
- updates: Partial<Package>,
74
- ): Promise<Package> {
75
- const existing = await this.getPackageById(id);
76
- if (!existing) {
77
- throw new Error(`Package with id ${id} not found`);
78
- }
79
-
80
- const now = this.now();
81
- const setClauses: string[] = [];
82
- const params: unknown[] = [];
83
-
84
- if (updates.name !== undefined) {
85
- setClauses.push(`name = ?`);
86
- params.push(updates.name);
87
- }
88
- if (updates.description !== undefined) {
89
- setClauses.push(`description = ?`);
90
- params.push(updates.description);
91
- }
92
- if (updates.manifestPath !== undefined) {
93
- setClauses.push(`manifest_path = ?`);
94
- params.push(updates.manifestPath);
95
- }
96
- if (updates.metadata !== undefined) {
97
- setClauses.push(`metadata = ?`);
98
- params.push(JSON.stringify(updates.metadata));
99
- }
100
-
101
- setClauses.push(`updated_at = ?`);
102
- params.push(now.toISOString());
103
- params.push(id);
104
-
105
- await this.db.run(
106
- `UPDATE packages SET ${setClauses.join(", ")} WHERE id = ?`,
107
- params,
108
- );
109
-
110
- return this.getPackageById(id) as Promise<Package>;
111
- }
112
-
113
- async deletePackage(id: string): Promise<void> {
114
- await this.db.run("DELETE FROM packages WHERE id = ?", [id]);
115
- }
116
-
117
- async deletePackagesByProjectId(id: string): Promise<void> {
118
- await this.db.run("DELETE FROM packages WHERE project_id = ?", [id]);
119
- }
120
-
121
- private mapToPackage(row: Record<string, unknown>): Package {
122
- return {
123
- id: row.id as string,
124
- projectId: row.project_id as string,
125
- name: row.name as string,
126
- description: row.description as string | undefined,
127
- manifestPath: row.manifest_path as string,
128
- metadata: row.metadata
129
- ? JSON.parse(row.metadata as string)
130
- : undefined,
131
- createdAt: new Date(row.created_at as string),
132
- updatedAt: new Date(row.updated_at as string),
133
- };
134
- }
135
- }
@@ -1,148 +0,0 @@
1
- import { Project } from "../DatabaseInterface";
2
- import { DuckDBConnection } from "./DuckDBConnection";
3
-
4
- export class ProjectRepository {
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 listProjects(): Promise<Project[]> {
16
- const rows = await this.db.all<Record<string, unknown>>(
17
- "SELECT * FROM projects ORDER BY name",
18
- );
19
- return rows.map(this.mapToProject);
20
- }
21
-
22
- async getProjectById(id: string): Promise<Project | null> {
23
- const row = await this.db.get<Record<string, unknown>>(
24
- "SELECT * FROM projects WHERE id = ?",
25
- [id],
26
- );
27
- return row ? this.mapToProject(row) : null;
28
- }
29
-
30
- async getProjectByName(name: string): Promise<Project | null> {
31
- const row = await this.db.get<Record<string, unknown>>(
32
- "SELECT * FROM projects WHERE name = ?",
33
- [name],
34
- );
35
- return row ? this.mapToProject(row) : null;
36
- }
37
-
38
- async createProject(
39
- project: Omit<Project, "id" | "createdAt" | "updatedAt">,
40
- ): Promise<Project> {
41
- const id = this.generateId();
42
- const now = this.now();
43
-
44
- const params = [
45
- id,
46
- project.name,
47
- project.path,
48
- project.description || null,
49
- project.metadata ? JSON.stringify(project.metadata) : null,
50
- now.toISOString(),
51
- now.toISOString(),
52
- ];
53
-
54
- try {
55
- await this.db.run(
56
- `INSERT INTO projects (id, name, path, description, metadata, created_at, updated_at)
57
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
58
- params,
59
- );
60
-
61
- return {
62
- id,
63
- ...project,
64
- createdAt: now,
65
- updatedAt: now,
66
- };
67
- } catch (err: unknown) {
68
- const error = err as Error;
69
- // If unique constraint violation, return existing project
70
- if (
71
- error.message?.includes("UNIQUE") ||
72
- error.message?.includes("Constraint")
73
- ) {
74
- const existing = await this.db.get<Record<string, unknown>>(
75
- "SELECT * FROM projects WHERE name = ?",
76
- [project.name],
77
- );
78
- if (existing) {
79
- console.log("Returning existing project");
80
- return this.mapToProject(existing);
81
- }
82
- }
83
- throw error;
84
- }
85
- }
86
-
87
- async updateProject(
88
- id: string,
89
- updates: Partial<Project>,
90
- ): Promise<Project> {
91
- const existing = await this.getProjectById(id);
92
- if (!existing) {
93
- throw new Error(`Project with id ${id} not found`);
94
- }
95
-
96
- const now = this.now();
97
- const setClauses: string[] = [];
98
- const params: unknown[] = [];
99
-
100
- if (updates.name !== undefined && updates.name !== existing.name) {
101
- setClauses.push(`name = ?`);
102
- params.push(updates.name);
103
- }
104
-
105
- if (updates.path !== undefined && updates.path !== existing.path) {
106
- setClauses.push(`path = ?`);
107
- params.push(updates.path);
108
- }
109
-
110
- if (updates.description !== undefined) {
111
- setClauses.push(`description = ?`);
112
- params.push(updates.description);
113
- }
114
- if (updates.metadata !== undefined) {
115
- setClauses.push(`metadata = ?`);
116
- params.push(JSON.stringify(updates.metadata));
117
- }
118
-
119
- setClauses.push(`updated_at = ?`);
120
- params.push(now.toISOString());
121
- params.push(id);
122
-
123
- await this.db.run(
124
- `UPDATE projects SET ${setClauses.join(", ")} WHERE id = ?`,
125
- params,
126
- );
127
-
128
- return this.getProjectById(id) as Promise<Project>;
129
- }
130
-
131
- async deleteProject(id: string): Promise<void> {
132
- await this.db.run("DELETE FROM projects WHERE id = ?", [id]);
133
- }
134
-
135
- private mapToProject(row: Record<string, unknown>): Project {
136
- return {
137
- id: row.id as string,
138
- name: row.name as string,
139
- path: row.path as string,
140
- description: row.description as string | undefined,
141
- metadata: row.metadata
142
- ? JSON.parse(row.metadata as string)
143
- : undefined,
144
- createdAt: new Date(row.created_at as string),
145
- updatedAt: new Date(row.updated_at as string),
146
- };
147
- }
148
- }
@@ -1,133 +0,0 @@
1
- import { beforeEach, describe, expect, it } from "bun:test";
2
- import * as sinon from "sinon";
3
- import { ManifestEntry, ResourceRepository } from "../DatabaseInterface";
4
- import { DuckDBManifestStore } from "./DuckDBManifestStore";
5
-
6
- function makeEntry(overrides: Partial<ManifestEntry> = {}): ManifestEntry {
7
- return {
8
- id: "entry-1",
9
- projectId: "proj-1",
10
- packageName: "pkg",
11
- buildId: "build-abc",
12
- tableName: "my_table",
13
- sourceName: "my_source",
14
- connectionName: "duckdb",
15
- createdAt: new Date("2026-04-03"),
16
- updatedAt: new Date("2026-04-03"),
17
- ...overrides,
18
- };
19
- }
20
-
21
- function createMocks() {
22
- const sandbox = sinon.createSandbox();
23
-
24
- const repository = {
25
- listManifestEntries: sandbox.stub(),
26
- upsertManifestEntry: sandbox.stub(),
27
- deleteManifestEntry: sandbox.stub(),
28
- };
29
-
30
- const store = new DuckDBManifestStore(
31
- repository as unknown as ResourceRepository,
32
- );
33
-
34
- return { sandbox, repository, store };
35
- }
36
-
37
- describe("DuckDBManifestStore", () => {
38
- let ctx: ReturnType<typeof createMocks>;
39
-
40
- beforeEach(() => {
41
- ctx = createMocks();
42
- });
43
-
44
- describe("getManifest", () => {
45
- it("should assemble a BuildManifest from repository entries", async () => {
46
- ctx.repository.listManifestEntries.resolves([
47
- makeEntry({ buildId: "b1", tableName: "tbl_a" }),
48
- makeEntry({ buildId: "b2", tableName: "tbl_b" }),
49
- ]);
50
-
51
- const manifest = await ctx.store.getManifest("proj-1", "pkg");
52
-
53
- expect(manifest.strict).toBe(false);
54
- expect(manifest.entries).toEqual({
55
- b1: { tableName: "tbl_a" },
56
- b2: { tableName: "tbl_b" },
57
- });
58
- });
59
-
60
- it("should return empty entries when no rows exist", async () => {
61
- ctx.repository.listManifestEntries.resolves([]);
62
-
63
- const manifest = await ctx.store.getManifest("proj-1", "pkg");
64
-
65
- expect(manifest.strict).toBe(false);
66
- expect(manifest.entries).toEqual({});
67
- });
68
- });
69
-
70
- describe("writeEntry", () => {
71
- it("should upsert an entry with all fields", async () => {
72
- ctx.repository.upsertManifestEntry.resolves(makeEntry());
73
-
74
- await ctx.store.writeEntry(
75
- "proj-1",
76
- "pkg",
77
- "build-abc",
78
- "tbl",
79
- "src",
80
- "conn",
81
- );
82
-
83
- expect(ctx.repository.upsertManifestEntry.calledOnce).toBe(true);
84
- const arg = ctx.repository.upsertManifestEntry.firstCall.args[0];
85
- expect(arg).toEqual({
86
- projectId: "proj-1",
87
- packageName: "pkg",
88
- buildId: "build-abc",
89
- tableName: "tbl",
90
- sourceName: "src",
91
- connectionName: "conn",
92
- });
93
- });
94
- });
95
-
96
- describe("deleteEntry", () => {
97
- it("should delegate to repository", async () => {
98
- ctx.repository.deleteManifestEntry.resolves();
99
-
100
- await ctx.store.deleteEntry("entry-1");
101
-
102
- expect(ctx.repository.deleteManifestEntry.calledOnce).toBe(true);
103
- expect(ctx.repository.deleteManifestEntry.firstCall.args[0]).toBe(
104
- "entry-1",
105
- );
106
- });
107
- });
108
-
109
- describe("listEntries", () => {
110
- it("should return entries from the repository", async () => {
111
- const entries = [
112
- makeEntry(),
113
- makeEntry({ id: "entry-2", buildId: "build-def" }),
114
- ];
115
- ctx.repository.listManifestEntries.resolves(entries);
116
-
117
- const result = await ctx.store.listEntries("proj-1", "pkg");
118
-
119
- expect(result).toEqual(entries);
120
- expect(
121
- ctx.repository.listManifestEntries.calledWith("proj-1", "pkg"),
122
- ).toBe(true);
123
- });
124
-
125
- it("should return empty array when no entries exist", async () => {
126
- ctx.repository.listManifestEntries.resolves([]);
127
-
128
- const result = await ctx.store.listEntries("proj-1", "pkg");
129
-
130
- expect(result).toEqual([]);
131
- });
132
- });
133
- });