@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.
- package/README.docker.md +145 -20
- package/README.md +65 -12
- package/dist/app/api-doc.html +5 -0
- package/dist/app/api-doc.yaml +3903 -583
- package/dist/app/assets/EnvironmentPage-DiUgLuCH.js +1 -0
- package/dist/app/assets/HomePage-MhhjhnFj.js +1 -0
- package/dist/app/assets/LightMode-BEyvNUzT.js +1 -0
- package/dist/app/assets/MainPage-D4Cfee9o.js +2 -0
- package/dist/app/assets/MaterializationsPage-nGgGqwSN.js +1 -0
- package/dist/app/assets/ModelPage-DLAcEw-F.js +1 -0
- package/dist/app/assets/PackagePage-Df0ikH7w.js +1 -0
- package/dist/app/assets/RouteError-BFQwtroH.js +1 -0
- package/dist/app/assets/ThemeEditorPage-OoskoQYN.js +1 -0
- package/dist/app/assets/WorkbookPage-CpFgvUDC.js +1 -0
- package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-lb28vQkR.es-BoVSOc75.js} +1 -1
- package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
- package/dist/app/assets/index-5eLCcNmP.css +1 -0
- package/dist/app/assets/index-6ypuFH6g.js +1774 -0
- package/dist/app/assets/index-B8pnU_V_.js +615 -0
- package/dist/app/assets/index-BImcHmVP.js +1 -0
- package/dist/app/assets/index-DSoCUF-f.js +2725 -0
- package/dist/app/assets/index-DfqzSItA.js +2 -0
- package/dist/app/assets/index-w_0OQJgZ.js +23 -0
- package/dist/app/index.html +8 -4
- package/dist/cpufeatures-1yrn0vtw.node +0 -0
- package/dist/default-publisher.config.json +23 -0
- package/dist/{instrumentation.js → instrumentation.mjs} +10631 -10573
- package/dist/package_load_worker.mjs +15219 -0
- package/dist/runtime/publisher.js +331 -0
- package/dist/{server.js → server.mjs} +115548 -49590
- package/dist/sshcrypto-vd2k5hq9.node +0 -0
- package/package.json +41 -28
- package/publisher.config.example.bigquery.json +33 -0
- package/publisher.config.example.duckdb.json +23 -0
- package/.eslintrc.json +0 -44
- package/.prettierignore +0 -1
- package/.prettierrc +0 -4
- package/build.ts +0 -38
- package/dist/app/assets/HomePage-Dn3E4CuB.js +0 -1
- package/dist/app/assets/MainPage-BzB3yoqi.js +0 -2
- package/dist/app/assets/ModelPage-C9O_sAXT.js +0 -1
- package/dist/app/assets/PackagePage-DcxKEjBX.js +0 -1
- package/dist/app/assets/ProjectPage-BDj307rF.js +0 -1
- package/dist/app/assets/RouteError-DAShbVCG.js +0 -1
- package/dist/app/assets/WorkbookPage-Cs_XYEaB.js +0 -1
- package/dist/app/assets/index-15BOvhp0.js +0 -456
- package/dist/app/assets/index-Bb2jqquW.js +0 -676
- package/dist/app/assets/index-CMlGQMcl.css +0 -1
- package/dist/app/assets/index-D68X76-7.js +0 -1276
- package/dist/app/assets/index.umd-DGBekgSu.js +0 -1145
- package/dxt/malloy_bridge.py +0 -354
- package/dxt/manifest.json +0 -22
- package/eslint.config.mjs +0 -76
- package/k6-tests/README.md +0 -144
- package/k6-tests/bun.lock +0 -797
- package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
- package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
- package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
- package/k6-tests/load-test/load-test-crud.ts +0 -142
- package/k6-tests/load-test/load-test.ts +0 -784
- package/k6-tests/package.json +0 -27
- package/k6-tests/scripts/run-with-otel.sh +0 -50
- package/k6-tests/smoke-test/smoke-test.ts +0 -158
- package/k6-tests/tsconfig.json +0 -29
- package/k6-tests/utils/client_factory.ts +0 -128
- package/k6-tests/utils/common.ts +0 -489
- package/k6-tests/utils/logger.ts +0 -62
- package/k6-tests/utils/types.d.ts +0 -35
- package/malloy_mcp.dxt +0 -0
- package/publisher.config.json +0 -33
- package/src/config.spec.ts +0 -850
- package/src/config.ts +0 -305
- package/src/constants.ts +0 -10
- package/src/controller/compile.controller.ts +0 -35
- package/src/controller/connection.controller.ts +0 -515
- package/src/controller/database.controller.ts +0 -21
- package/src/controller/manifest.controller.ts +0 -29
- package/src/controller/materialization.controller.ts +0 -125
- package/src/controller/model.controller.ts +0 -107
- package/src/controller/package.controller.ts +0 -182
- package/src/controller/query.controller.ts +0 -60
- package/src/controller/watch-mode.controller.ts +0 -98
- package/src/data_styles.ts +0 -71
- package/src/dto/connection.dto.spec.ts +0 -131
- package/src/dto/connection.dto.ts +0 -174
- package/src/dto/index.ts +0 -2
- package/src/dto/package.dto.spec.ts +0 -42
- package/src/dto/package.dto.ts +0 -12
- package/src/dto/validate.spec.ts +0 -76
- package/src/dto/validate.ts +0 -31
- package/src/errors.ts +0 -116
- package/src/health.ts +0 -189
- package/src/instrumentation.ts +0 -148
- package/src/logger.ts +0 -154
- package/src/mcp/error_messages.ts +0 -199
- package/src/mcp/handler_utils.ts +0 -239
- package/src/mcp/mcp_constants.ts +0 -22
- package/src/mcp/prompts/handlers.ts +0 -84
- package/src/mcp/prompts/index.ts +0 -11
- package/src/mcp/prompts/prompt_definitions.ts +0 -160
- package/src/mcp/prompts/prompt_service.ts +0 -67
- package/src/mcp/prompts/utils.ts +0 -62
- package/src/mcp/resource_metadata.ts +0 -47
- package/src/mcp/resources/model_resource.ts +0 -153
- package/src/mcp/resources/notebook_resource.ts +0 -137
- package/src/mcp/resources/package_resource.ts +0 -370
- package/src/mcp/resources/project_resource.ts +0 -184
- package/src/mcp/resources/query_resource.ts +0 -117
- package/src/mcp/resources/source_resource.ts +0 -141
- package/src/mcp/resources/view_resource.ts +0 -136
- package/src/mcp/server.ts +0 -62
- package/src/mcp/tools/discovery_tools.ts +0 -258
- package/src/mcp/tools/execute_query_tool.ts +0 -265
- package/src/server.ts +0 -1195
- package/src/service/connection.spec.ts +0 -1331
- package/src/service/connection.ts +0 -1511
- package/src/service/connection_service.spec.ts +0 -641
- package/src/service/connection_service.ts +0 -181
- package/src/service/db_utils.spec.ts +0 -714
- package/src/service/db_utils.ts +0 -1227
- package/src/service/filter.spec.ts +0 -447
- package/src/service/filter.ts +0 -337
- package/src/service/filter_integration.spec.ts +0 -825
- package/src/service/gcs_s3_utils.ts +0 -427
- package/src/service/manifest_service.spec.ts +0 -201
- package/src/service/manifest_service.ts +0 -106
- package/src/service/materialization_service.spec.ts +0 -648
- package/src/service/materialization_service.ts +0 -929
- package/src/service/materialized_table_gc.spec.ts +0 -383
- package/src/service/materialized_table_gc.ts +0 -279
- package/src/service/model.spec.ts +0 -257
- package/src/service/model.ts +0 -1069
- package/src/service/package.spec.ts +0 -236
- package/src/service/package.ts +0 -463
- package/src/service/project.ts +0 -681
- package/src/service/project_compile.spec.ts +0 -197
- package/src/service/project_store.spec.ts +0 -859
- package/src/service/project_store.ts +0 -1531
- package/src/service/quoting.ts +0 -41
- package/src/service/resolve_project.ts +0 -13
- package/src/storage/BaseRepository.ts +0 -31
- package/src/storage/DatabaseInterface.ts +0 -180
- package/src/storage/StorageManager.mock.ts +0 -59
- package/src/storage/StorageManager.ts +0 -210
- package/src/storage/duckdb/ConnectionRepository.ts +0 -141
- package/src/storage/duckdb/DuckDBConnection.ts +0 -177
- package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
- package/src/storage/duckdb/DuckDBRepository.ts +0 -224
- package/src/storage/duckdb/ManifestRepository.ts +0 -119
- package/src/storage/duckdb/MaterializationRepository.ts +0 -249
- package/src/storage/duckdb/PackageRepository.ts +0 -135
- package/src/storage/duckdb/ProjectRepository.ts +0 -148
- package/src/storage/duckdb/manifest_store.spec.ts +0 -133
- package/src/storage/duckdb/schema.ts +0 -147
- package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -146
- package/src/utils.ts +0 -13
- package/tests/fixtures/persist-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-test/persist_test.malloy +0 -11
- package/tests/fixtures/persist-test/publisher.json +0 -5
- package/tests/fixtures/publisher.config.json +0 -15
- package/tests/harness/e2e.ts +0 -96
- package/tests/harness/mcp_test_setup.ts +0 -344
- package/tests/harness/mocks.ts +0 -37
- package/tests/harness/rest_e2e.ts +0 -68
- package/tests/harness/uris.ts +0 -31
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -470
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -434
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -663
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -122
- package/tests/integration/mcp/setup.spec.ts +0 -5
- package/tests/unit/duckdb/attached_databases.test.ts +0 -1132
- package/tests/unit/ducklake/ducklake.test.ts +0 -950
- package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
- package/tests/unit/mcp/prompt_happy.test.ts +0 -51
- 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
|
-
});
|